diff --git a/apps/applications/api/__init__.py b/apps/applications/api/__init__.py index e6bc7adb4..a707cfde6 100644 --- a/apps/applications/api/__init__.py +++ b/apps/applications/api/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/apps/applications/api/database_app.py b/apps/applications/api/database_app.py new file mode 100644 index 000000000..af5810e0f --- /dev/null +++ b/apps/applications/api/database_app.py @@ -0,0 +1,20 @@ +# coding: utf-8 +# + +from orgs.mixins.api import OrgBulkModelViewSet + +from .. import models +from .. import serializers +from ..hands import IsOrgAdminOrAppUser + +__all__ = [ + 'DatabaseAppViewSet', +] + + +class DatabaseAppViewSet(OrgBulkModelViewSet): + model = models.DatabaseApp + filter_fields = ('name',) + search_fields = filter_fields + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.DatabaseAppSerializer diff --git a/apps/applications/const.py b/apps/applications/const.py index a5b6da895..af3531c36 100644 --- a/apps/applications/const.py +++ b/apps/applications/const.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ # RemoteApp + REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor' REMOTE_APP_TYPE_CHROME = 'chrome' @@ -12,29 +13,6 @@ REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench' REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client' REMOTE_APP_TYPE_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 = [ @@ -60,9 +38,26 @@ REMOTE_APP_TYPE_CUSTOM_FIELDS = [ {'name': 'custom_password', 'write_only': True} ] -REMOTE_APP_TYPE_MAP_FIELDS = { +REMOTE_APP_TYPE_FIELDS_MAP = { REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS, REMOTE_APP_TYPE_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 } + +REMOTE_APP_TYPE_CHOICES = ( + (REMOTE_APP_TYPE_CHROME, 'Chrome'), + (REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'), + (REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'), + (REMOTE_APP_TYPE_CUSTOM, _('Custom')), +) + + +# DatabaseApp + + +DATABASE_APP_TYPE_MYSQL = 'mysql' + +DATABASE_APP_TYPE_CHOICES = ( + (DATABASE_APP_TYPE_MYSQL, 'MySQL'), +) diff --git a/apps/applications/forms/__init__.py b/apps/applications/forms/__init__.py index e6bc7adb4..a707cfde6 100644 --- a/apps/applications/forms/__init__.py +++ b/apps/applications/forms/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/apps/applications/forms/database_app.py b/apps/applications/forms/database_app.py new file mode 100644 index 000000000..2b5a4c0cf --- /dev/null +++ b/apps/applications/forms/database_app.py @@ -0,0 +1,26 @@ +# coding: utf-8 +# + + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .. import models + +__all__ = ['DatabaseAppMySQLForm'] + + +class BaseDatabaseAppForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['type'].widget.attrs['disabled'] = True + + class Meta: + model = models.DatabaseApp + fields = [ + 'name', 'type', 'host', 'port', 'database', 'comment' + ] + + +class DatabaseAppMySQLForm(BaseDatabaseAppForm): + pass diff --git a/apps/applications/forms/remote_app.py b/apps/applications/forms/remote_app.py index b12759462..7a097fcc8 100644 --- a/apps/applications/forms/remote_app.py +++ b/apps/applications/forms/remote_app.py @@ -5,18 +5,52 @@ from django.utils.translation import ugettext as _ from django import forms from orgs.mixins.forms import OrgModelForm -from assets.models import SystemUser from ..models import RemoteApp -from .. import const __all__ = [ - 'RemoteAppCreateUpdateForm', + 'RemoteAppChromeForm', 'RemoteAppMySQLWorkbenchForm', + 'RemoteAppVMwareForm', 'RemoteAppCustomForm' ] -class RemoteAppTypeChromeForm(forms.ModelForm): +class BaseRemoteAppForm(OrgModelForm): + default_initial_data = {} + + def __init__(self, *args, **kwargs): + # 过滤RDP资产和系统用户 + super().__init__(*args, **kwargs) + field_asset = self.fields['asset'] + field_asset.queryset = field_asset.queryset.has_protocol('rdp') + self.fields['type'].widget.attrs['disabled'] = True + self.fields.move_to_end('comment') + self.initial_default() + + def initial_default(self): + for name, value in self.default_initial_data.items(): + field = self.fields.get(name) + if not field: + continue + field.initial = value + + class Meta: + model = RemoteApp + fields = [ + 'name', 'asset', 'type', 'path', 'comment' + ] + widgets = { + 'asset': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Asset') + }), + } + + +class RemoteAppChromeForm(BaseRemoteAppForm): + default_initial_data = { + 'path': r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' + } + chrome_target = forms.CharField( max_length=128, label=_('Target URL'), required=False ) @@ -29,7 +63,12 @@ class RemoteAppTypeChromeForm(forms.ModelForm): ) -class RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm): +class RemoteAppMySQLWorkbenchForm(BaseRemoteAppForm): + default_initial_data = { + 'path': r'C:\Program Files\MySQL\MySQL Workbench 8.0 CE' + r'\MySQLWorkbench.exe' + } + mysql_workbench_ip = forms.CharField( max_length=128, label=_('Database IP'), required=False ) @@ -45,7 +84,12 @@ class RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm): ) -class RemoteAppTypeVMwareForm(forms.ModelForm): +class RemoteAppVMwareForm(BaseRemoteAppForm): + default_initial_data = { + 'path': r'C:\Program Files (x86)\VMware\Infrastructure' + r'\Virtual Infrastructure Client\Launcher\VpxClient.exe' + } + vmware_target = forms.CharField( max_length=128, label=_('Target address'), required=False ) @@ -58,7 +102,8 @@ class RemoteAppTypeVMwareForm(forms.ModelForm): ) -class RemoteAppTypeCustomForm(forms.ModelForm): +class RemoteAppCustomForm(BaseRemoteAppForm): + custom_cmdline = forms.CharField( max_length=128, label=_('Operating parameter'), required=False ) @@ -73,51 +118,3 @@ class RemoteAppTypeCustomForm(forms.ModelForm): max_length=128, label=_('Login password'), required=False ) - -class RemoteAppTypeForms( - RemoteAppTypeChromeForm, - RemoteAppTypeMySQLWorkbenchForm, - RemoteAppTypeVMwareForm, - RemoteAppTypeCustomForm -): - pass - - -class RemoteAppCreateUpdateForm(RemoteAppTypeForms, OrgModelForm): - def __init__(self, *args, **kwargs): - # 过滤RDP资产和系统用户 - super().__init__(*args, **kwargs) - field_asset = self.fields['asset'] - field_asset.queryset = field_asset.queryset.has_protocol('rdp') - - class Meta: - model = RemoteApp - fields = [ - 'name', 'asset', 'type', 'path', 'comment' - ] - widgets = { - 'asset': forms.Select(attrs={ - 'class': 'select2', 'data-placeholder': _('Asset') - }), - } - - def _clean_params(self): - app_type = self.data.get('type') - fields = const.REMOTE_APP_TYPE_MAP_FIELDS.get(app_type, []) - params = {} - for field in fields: - name = field['name'] - value = self.cleaned_data[name] - params.update({name: value}) - return params - - def _save_params(self, instance): - params = self._clean_params() - instance.params = params - instance.save() - return instance - - def save(self, commit=True): - instance = super().save(commit=commit) - instance = self._save_params(instance) - return instance diff --git a/apps/applications/migrations/0003_auto_20191210_1659.py b/apps/applications/migrations/0003_auto_20191210_1659.py new file mode 100644 index 000000000..fc3e4cdf5 --- /dev/null +++ b/apps/applications/migrations/0003_auto_20191210_1659.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.11 on 2019-12-10 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0002_remove_remoteapp_system_user'), + ] + + operations = [ + migrations.AlterField( + model_name='remoteapp', + name='type', + field=models.CharField(choices=[('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type'), + ), + ] diff --git a/apps/applications/migrations/0004_auto_20191218_1705.py b/apps/applications/migrations/0004_auto_20191218_1705.py new file mode 100644 index 000000000..f22d2e290 --- /dev/null +++ b/apps/applications/migrations/0004_auto_20191218_1705.py @@ -0,0 +1,38 @@ +# Generated by Django 2.1.11 on 2019-12-18 09:05 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0003_auto_20191210_1659'), + ] + + operations = [ + migrations.CreateModel( + name='DatabaseApp', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('type', models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=128, verbose_name='Type')), + ('host', models.CharField(db_index=True, max_length=128, verbose_name='Host')), + ('port', models.IntegerField(default=3306, verbose_name='Port')), + ('database', models.CharField(blank=True, db_index=True, max_length=128, null=True, verbose_name='Database')), + ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ], + options={ + 'verbose_name': 'DatabaseApp', + 'ordering': ('name',), + }, + ), + migrations.AlterUniqueTogether( + name='databaseapp', + unique_together={('org_id', 'name')}, + ), + ] diff --git a/apps/applications/models/__init__.py b/apps/applications/models/__init__.py index e6bc7adb4..a707cfde6 100644 --- a/apps/applications/models/__init__.py +++ b/apps/applications/models/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/apps/applications/models/database_app.py b/apps/applications/models/database_app.py new file mode 100644 index 000000000..3317f06a4 --- /dev/null +++ b/apps/applications/models/database_app.py @@ -0,0 +1,42 @@ +# coding: utf-8 +# + +import uuid +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orgs.mixins.models import OrgModelMixin +from common.mixins import CommonModelMixin +from .. import const + + +__all__ = ['DatabaseApp'] + + +class DatabaseApp(CommonModelMixin, OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_('Name')) + type = models.CharField( + default=const.DATABASE_APP_TYPE_MYSQL, + choices=const.DATABASE_APP_TYPE_CHOICES, + max_length=128, verbose_name=_('Type') + ) + host = models.CharField( + max_length=128, verbose_name=_('Host'), db_index=True + ) + port = models.IntegerField(default=3306, verbose_name=_('Port')) + database = models.CharField( + max_length=128, blank=True, null=True, verbose_name=_('Database'), + db_index=True + ) + comment = models.TextField( + max_length=128, default='', blank=True, verbose_name=_('Comment') + ) + + def __str__(self): + return self.name + + class Meta: + unique_together = [('org_id', 'name'), ] + verbose_name = _("DatabaseApp") + ordering = ('name', ) diff --git a/apps/applications/models/remote_app.py b/apps/applications/models/remote_app.py index 17746833c..b9aee0ade 100644 --- a/apps/applications/models/remote_app.py +++ b/apps/applications/models/remote_app.py @@ -62,7 +62,7 @@ class RemoteApp(OrgModelMixin): _parameters.append(self.type) path = '\"%s\"' % self.path _parameters.append(path) - for field in const.REMOTE_APP_TYPE_MAP_FIELDS[self.type]: + for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]: value = self.params.get(field['name']) if value is None: continue diff --git a/apps/applications/serializers/__init__.py b/apps/applications/serializers/__init__.py index e6bc7adb4..a707cfde6 100644 --- a/apps/applications/serializers/__init__.py +++ b/apps/applications/serializers/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/apps/applications/serializers/database_app.py b/apps/applications/serializers/database_app.py new file mode 100644 index 000000000..43b873193 --- /dev/null +++ b/apps/applications/serializers/database_app.py @@ -0,0 +1,26 @@ +# coding: utf-8 +# + +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.serializers import AdaptedBulkListSerializer + +from .. import models + +__all__ = [ + 'DatabaseAppSerializer', +] + + +class DatabaseAppSerializer(BulkOrgResourceModelSerializer): + + class Meta: + model = models.DatabaseApp + list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'name', 'type', 'get_type_display', 'host', 'port', + 'database', 'comment', 'created_by', 'date_created', 'date_updated', + ] + read_only_fields = [ + 'created_by', 'date_created', 'date_updated' + 'get_type_display', + ] diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py index 168a02280..fb9a7a270 100644 --- a/apps/applications/serializers/remote_app.py +++ b/apps/applications/serializers/remote_app.py @@ -1,10 +1,11 @@ # coding: utf-8 # - +import copy from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer +from common.fields.serializer import CustomMetaDictField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .. import const @@ -16,72 +17,54 @@ __all__ = [ ] -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 RemoteAppParamsDictField(CustomMetaDictField): + type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP + default_type = const.REMOTE_APP_TYPE_CHROME + convert_key_remove_type_prefix = False + convert_key_to_upper = False class RemoteAppSerializer(BulkOrgResourceModelSerializer): params = RemoteAppParamsDictField() + type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP class Meta: model = RemoteApp list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'asset', 'type', 'path', 'params', - 'comment', 'created_by', 'date_created', 'asset_info', - 'get_type_display', + 'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display', + 'path', 'params', 'date_created', 'created_by', 'comment', ] read_only_fields = [ 'created_by', 'date_created', 'asset_info', 'get_type_display' ] + def process_params(self, instance, validated_data): + new_params = copy.deepcopy(validated_data.get('params', {})) + tp = validated_data.get('type', '') + + if tp != instance.type: + return new_params + + old_params = instance.params + fields = self.type_fields_map.get(instance.type, []) + for field in fields: + if not field.get('write_only', False): + continue + field_name = field['name'] + new_value = new_params.get(field_name, '') + old_value = old_params.get(field_name, '') + field_value = new_value if new_value else old_value + new_params[field_name] = field_value + + return new_params + + def update(self, instance, validated_data): + params = self.process_params(instance, validated_data) + validated_data['params'] = params + return super().update(instance, validated_data) + class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer): parameter_remote_app = serializers.SerializerMethodField() diff --git a/apps/applications/templates/applications/database_app_create_update.html b/apps/applications/templates/applications/database_app_create_update.html new file mode 100644 index 000000000..84635e2d0 --- /dev/null +++ b/apps/applications/templates/applications/database_app_create_update.html @@ -0,0 +1,55 @@ +{% extends '_base_create_update.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} + +{% block form %} +
+ {% bootstrap_form form layout="horizontal" %} +
+
+
+ + +
+
+
+{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} \ No newline at end of file diff --git a/apps/applications/templates/applications/database_app_detail.html b/apps/applications/templates/applications/database_app_detail.html new file mode 100644 index 000000000..153bfa98a --- /dev/null +++ b/apps/applications/templates/applications/database_app_detail.html @@ -0,0 +1,103 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
+
+
+
+ +
+
+
+
+ {{ database_app.name }} +
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans 'Name' %}:{{ database_app.name }}
{% trans 'Type' %}:{{ database_app.get_type_display }}
{% trans 'Host' %}:{{ database_app.host }}
{% trans 'Port' %}:{{ database_app.port }}
{% trans 'Database' %}:{{ database_app.database }}
{% trans 'Date created' %}:{{ database_app.date_created }}
{% trans 'Created by' %}:{{ database_app.created_by }}
{% trans 'Comment' %}:{{ database_app.comment }}
+
+
+
+
+
+
+
+
+{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/applications/templates/applications/database_app_list.html b/apps/applications/templates/applications/database_app_list.html new file mode 100644 index 000000000..5b60467b1 --- /dev/null +++ b/apps/applications/templates/applications/database_app_list.html @@ -0,0 +1,89 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block help_message %} +{% endblock %} +{% block table_search %}{% endblock %} +{% block table_container %} +
+ + + +
+ + + + + + + + + + + + + + + +
+ + {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Port' %}{% trans 'Database' %}{% trans 'Comment' %}{% trans 'Action' %}
+{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/applications/templates/applications/remote_app_create_update.html b/apps/applications/templates/applications/remote_app_create_update.html index b193dfff5..440219936 100644 --- a/apps/applications/templates/applications/remote_app_create_update.html +++ b/apps/applications/templates/applications/remote_app_create_update.html @@ -4,51 +4,8 @@ {% load i18n %} {% block form %} -
- {% if form.non_field_errors %} -
- {{ form.non_field_errors }} -
- {% endif %} - {% csrf_token %} - {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.asset layout="horizontal" %} - {% bootstrap_field form.type layout="horizontal" %} - {% bootstrap_field form.path layout="horizontal" %} - -
- - {# chrome #} -
- {% bootstrap_field form.chrome_target layout="horizontal" %} - {% bootstrap_field form.chrome_username layout="horizontal" %} - {% bootstrap_field form.chrome_password layout="horizontal" %} -
- - {# mysql workbench #} -
- {% 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" %} -
- - {# vmware #} -
- {% bootstrap_field form.vmware_target layout="horizontal" %} - {% bootstrap_field form.vmware_username layout="horizontal" %} - {% bootstrap_field form.vmware_password layout="horizontal" %} -
- - {# custom #} -
- {% 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" %} -
- - {% bootstrap_field form.comment layout="horizontal" %} + + {% bootstrap_form form layout="horizontal" %}
@@ -57,93 +14,49 @@
-
{% endblock %} {% block custom_foot_js %} -{% endblock %} - {% block content %}
@@ -102,4 +97,4 @@ $(document).ready(function () { objectDelete($this, name, the_url, redirect_url); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/applications/templates/applications/remote_app_list.html b/apps/applications/templates/applications/remote_app_list.html index a308bf911..72ba0186b 100644 --- a/apps/applications/templates/applications/remote_app_list.html +++ b/apps/applications/templates/applications/remote_app_list.html @@ -6,8 +6,16 @@ {% endblock %} {% block table_search %}{% endblock %} {% block table_container %} -
- {% trans "Create RemoteApp" %} +
+ + +
@@ -61,7 +69,7 @@ function initTable() { {data: "get_type_display", orderable: false}, {data: "asset_info", orderable: false}, {data: "comment"}, - {data: "id", orderable: false} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/apps/applications/templates/applications/user_database_app_list.html b/apps/applications/templates/applications/user_database_app_list.html new file mode 100644 index 000000000..1edaacd76 --- /dev/null +++ b/apps/applications/templates/applications/user_database_app_list.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block custom_head_css_js %} + +{% endblock %} + +{% block content %} +
+
+ + + + + + + + + + + + + +
+ + {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Database' %}{% trans 'Comment' %}{% trans 'Action' %}
+
+{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py index 6384f5dac..1186bf1a2 100644 --- a/apps/applications/urls/api_urls.py +++ b/apps/applications/urls/api_urls.py @@ -11,10 +11,12 @@ app_name = 'applications' router = BulkRouter() router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') +router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app') urlpatterns = [ path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), ] + old_version_urlpatterns = [ re_path('(?Premote-app)/.*', capi.redirect_plural_name_api) ] diff --git a/apps/applications/urls/views_urls.py b/apps/applications/urls/views_urls.py index 3ffcffc5c..663b0878d 100644 --- a/apps/applications/urls/views_urls.py +++ b/apps/applications/urls/views_urls.py @@ -11,6 +11,13 @@ urlpatterns = [ path('remote-app//update/', views.RemoteAppUpdateView.as_view(), name='remote-app-update'), path('remote-app//', views.RemoteAppDetailView.as_view(), name='remote-app-detail'), # User RemoteApp view - path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list') + path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list'), + + path('database-app/', views.DatabaseAppListView.as_view(), name='database-app-list'), + path('database-app/create/', views.DatabaseAppCreateView.as_view(), name='database-app-create'), + path('database-app//update/', views.DatabaseAppUpdateView.as_view(), name='database-app-update'), + path('database-app//', views.DatabaseAppDetailView.as_view(), name='database-app-detail'), + # User DatabaseApp view + path('user-database-app/', views.UserDatabaseAppListView.as_view(), name='user-database-app-list'), ] diff --git a/apps/applications/views/__init__.py b/apps/applications/views/__init__.py index e6bc7adb4..a707cfde6 100644 --- a/apps/applications/views/__init__.py +++ b/apps/applications/views/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/apps/applications/views/database_app.py b/apps/applications/views/database_app.py new file mode 100644 index 000000000..21d2b4f7c --- /dev/null +++ b/apps/applications/views/database_app.py @@ -0,0 +1,115 @@ +# coding: utf-8 +# + +from django.http import Http404 +from django.views.generic import TemplateView +from django.views.generic.edit import CreateView, UpdateView +from django.utils.translation import ugettext_lazy as _ +from django.views.generic.detail import DetailView + +from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser + +from .. import models, const, forms + +__all__ = [ + 'DatabaseAppListView', 'DatabaseAppCreateView', 'DatabaseAppUpdateView', + 'DatabaseAppDetailView', 'UserDatabaseAppListView', +] + + +class DatabaseAppListView(PermissionsMixin, TemplateView): + template_name = 'applications/database_app_list.html' + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _("Application"), + 'action': _('DatabaseApp list'), + 'type_choices': const.DATABASE_APP_TYPE_CHOICES + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class BaseDatabaseAppCreateUpdateView: + template_name = 'applications/database_app_create_update.html' + model = models.DatabaseApp + permission_classes = [IsOrgAdmin] + default_type = const.DATABASE_APP_TYPE_MYSQL + form_class = forms.DatabaseAppMySQLForm + form_class_choices = { + const.DATABASE_APP_TYPE_MYSQL: forms.DatabaseAppMySQLForm, + } + + def get_initial(self): + return {'type': self.get_type()} + + def get_type(self): + return self.default_type + + def get_form_class(self): + tp = self.get_type() + form_class = self.form_class_choices.get(tp) + if not form_class: + raise Http404() + return form_class + + +class DatabaseAppCreateView(BaseDatabaseAppCreateUpdateView, CreateView): + + def get_type(self): + tp = self.request.GET.get("type") + if tp: + return tp.lower() + return super().get_type() + + def get_context_data(self, **kwargs): + context = { + 'app': _('Applications'), + 'action': _('Create DatabaseApp'), + 'api_action': 'create' + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppUpdateView(BaseDatabaseAppCreateUpdateView, UpdateView): + + def get_type(self): + return self.object.type + + def get_context_data(self, **kwargs): + context = { + 'app': _('Applications'), + 'action': _('Create DatabaseApp'), + 'api_action': 'update' + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppDetailView(PermissionsMixin, DetailView): + template_name = 'applications/database_app_detail.html' + model = models.DatabaseApp + context_object_name = 'database_app' + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Applications'), + 'action': _('DatabaseApp detail'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserDatabaseAppListView(PermissionsMixin, TemplateView): + template_name = 'applications/user_database_app_list.html' + permission_classes = [IsValidUser] + + def get_context_data(self, **kwargs): + context = { + 'action': _('My DatabaseApp'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) diff --git a/apps/applications/views/remote_app.py b/apps/applications/views/remote_app.py index e7f6f0ccd..92b436005 100644 --- a/apps/applications/views/remote_app.py +++ b/apps/applications/views/remote_app.py @@ -1,19 +1,16 @@ # coding: utf-8 # +from django.http import Http404 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.urls import reverse_lazy - from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser -from common.const import create_success_msg, update_success_msg from ..models import RemoteApp -from .. import forms +from .. import forms, const __all__ = [ @@ -30,53 +27,79 @@ class RemoteAppListView(PermissionsMixin, TemplateView): context = { 'app': _('Applications'), 'action': _('RemoteApp list'), + 'type_choices': const.REMOTE_APP_TYPE_CHOICES, } kwargs.update(context) return super().get_context_data(**kwargs) -class RemoteAppCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): +class BaseRemoteAppCreateUpdateView: template_name = 'applications/remote_app_create_update.html' model = RemoteApp - form_class = forms.RemoteAppCreateUpdateForm - success_url = reverse_lazy('applications:remote-app-list') permission_classes = [IsOrgAdmin] + default_type = const.REMOTE_APP_TYPE_CHROME + form_class = forms.RemoteAppChromeForm + form_class_choices = { + const.REMOTE_APP_TYPE_CHROME: forms.RemoteAppChromeForm, + const.REMOTE_APP_TYPE_MYSQL_WORKBENCH: forms.RemoteAppMySQLWorkbenchForm, + const.REMOTE_APP_TYPE_VMWARE_CLIENT: forms.RemoteAppVMwareForm, + const.REMOTE_APP_TYPE_CUSTOM: forms.RemoteAppCustomForm + } + + def get_initial(self): + return {'type': self.get_type()} + + def get_type(self): + return self.default_type + + def get_form_class(self): + tp = self.get_type() + form_class = self.form_class_choices.get(tp) + if not form_class: + raise Http404() + return form_class + + +class RemoteAppCreateView(BaseRemoteAppCreateUpdateView, + PermissionsMixin, CreateView): def get_context_data(self, **kwargs): context = { 'app': _('Applications'), 'action': _('Create RemoteApp'), - 'type': 'create' + 'api_action': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - return create_success_msg % ({'name': cleaned_data['name']}) + def get_type(self): + tp = self.request.GET.get("type") + if tp: + return tp.lower() + return super().get_type() -class RemoteAppUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): - template_name = 'applications/remote_app_create_update.html' - model = RemoteApp - form_class = forms.RemoteAppCreateUpdateForm - success_url = reverse_lazy('applications:remote-app-list') - permission_classes = [IsOrgAdmin] +class RemoteAppUpdateView(BaseRemoteAppCreateUpdateView, + PermissionsMixin, UpdateView): def get_initial(self): - return {k: v for k, v in self.object.params.items()} + initial_data = super().get_initial() + params = {k: v for k, v in self.object.params.items()} + initial_data.update(params) + return initial_data + + def get_type(self): + return self.object.type def get_context_data(self, **kwargs): context = { 'app': _('Applications'), 'action': _('Update RemoteApp'), - 'type': 'update' + 'api_action': 'update' } 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(PermissionsMixin, DetailView): template_name = 'applications/remote_app_detail.html' diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index e7126878d..342f71bd4 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -2,6 +2,7 @@ from .admin_user import * from .asset import * from .label import * from .system_user import * +from .system_user_relation import * from .node import * from .domain import * from .cmd_filter import * diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index b91f10c65..52b05fcd4 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -14,6 +14,7 @@ # limitations under the License. from django.db import transaction +from django.db.models import Count from django.shortcuts import get_object_or_404 from rest_framework.response import Response from orgs.mixins.api import OrgBulkModelViewSet @@ -44,6 +45,11 @@ class AdminUserViewSet(OrgBulkModelViewSet): serializer_class = serializers.AdminUserSerializer permission_classes = (IsOrgAdmin,) + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(_assets_amount=Count('assets')) + return queryset + class AdminUserAuthApi(generics.UpdateAPIView): model = AdminUser diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 64fdc16dc..0b63522ea 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -4,24 +4,27 @@ import random from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +from rest_framework.generics import RetrieveAPIView from django.shortcuts import get_object_or_404 from common.utils import get_logger, get_object_or_none -from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics -from ..models import Asset, Node +from ..models import Asset, Node, Platform from .. import serializers -from ..tasks import update_asset_hardware_info_manual, \ - test_asset_connectivity_manual +from ..tasks import ( + update_asset_hardware_info_manual, test_asset_connectivity_manual +) from ..filters import AssetByNodeFilterBackend, LabelFilterBackend logger = get_logger(__file__) __all__ = [ - 'AssetViewSet', + 'AssetViewSet', 'AssetPlatformRetrieveApi', 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', - 'AssetGatewayApi', + 'AssetGatewayApi', 'AssetPlatformViewSet', ] @@ -53,6 +56,34 @@ class AssetViewSet(OrgBulkModelViewSet): self.set_assets_node(assets) +class AssetPlatformRetrieveApi(RetrieveAPIView): + queryset = Platform.objects.all() + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.PlatformSerializer + + def get_object(self): + asset_pk = self.kwargs.get('pk') + asset = get_object_or_404(Asset, pk=asset_pk) + return asset.platform + + +class AssetPlatformViewSet(ModelViewSet): + queryset = Platform.objects.all() + permission_classes = (IsSuperUser,) + serializer_class = serializers.PlatformSerializer + filterset_fields = ['name', 'base'] + search_fields = ['name'] + + def check_object_permissions(self, request, obj): + if request.method.lower() in ['delete', 'put', 'patch'] and \ + obj.internal: + self.permission_denied( + request, message={"detail": "Internal platform"} + ) + + return super().check_object_permissions(request, obj) + + class AssetRefreshHardwareApi(generics.RetrieveAPIView): """ Refresh asset hardware info diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index a07544e57..a3b9d8906 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -114,7 +114,7 @@ class AssetUserExportViewSet(AssetUserViewSet): permission_classes = [IsOrgAdminOrAppUser] def get_permissions(self): - if settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA: + if settings.SECURITY_VIEW_AUTH_NEED_MFA: self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] return super().get_permissions() @@ -124,7 +124,7 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView): permission_classes = [IsOrgAdminOrAppUser] def get_permissions(self): - if settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA: + if settings.SECURITY_VIEW_AUTH_NEED_MFA: self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] return super().get_permissions() diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 1451650d2..24661bad6 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -177,7 +177,7 @@ class NodeChildrenAsTreeApi(NodeChildrenApi): if not include_assets: return queryset assets = self.instance.get_assets().only( - "id", "hostname", "ip", 'platform', "os", + "id", "hostname", "ip", "os", "org_id", "protocols", ) for asset in assets: diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index f1213f0a3..e3dddd6f5 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -14,8 +14,8 @@ # limitations under the License. from django.shortcuts import get_object_or_404 -from django.conf import settings from rest_framework.response import Response +from django.db.models import Count from common.serializers import CeleryTaskSerializer from common.utils import get_logger @@ -50,6 +50,11 @@ class SystemUserViewSet(OrgBulkModelViewSet): serializer_class = serializers.SystemUserSerializer permission_classes = (IsOrgAdminOrAppUser,) + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(_assets_amount=Count('assets')) + return queryset + class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py new file mode 100644 index 000000000..de88cfe38 --- /dev/null +++ b/apps/assets/api/system_user_relation.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +from django.db.models import F, Value +from django.db.models.functions import Concat + +from common.permissions import IsOrgAdmin +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.utils import current_org +from .. import models, serializers + +__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet'] + + +class RelationMixin(OrgBulkModelViewSet): + def get_queryset(self): + queryset = self.model.objects.all() + org_id = current_org.org_id() + if org_id is not None: + queryset = queryset.filter(systemuser__org_id=org_id) + queryset = queryset.annotate(systemuser_display=Concat( + F('systemuser__name'), Value('('), F('systemuser__username'), + Value(')') + )) + return queryset + + +class SystemUserAssetRelationViewSet(RelationMixin): + serializer_class = serializers.SystemUserAssetRelationSerializer + model = models.SystemUser.assets.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'asset', 'systemuser', + ] + search_fields = [ + "id", "asset__hostname", "asset__ip", + "systemuser__name", "systemuser__username" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate( + asset_display=Concat( + F('asset__hostname'), Value('('), + F('asset__ip'), Value(')') + ) + ) + return queryset + + +class SystemUserNodeRelationViewSet(RelationMixin): + serializer_class = serializers.SystemUserNodeRelationSerializer + model = models.SystemUser.nodes.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'node', 'systemuser', + ] + search_fields = [ + "node__value", "systemuser__name", "systemuser_username" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(node_key=F('node__key')) + return queryset diff --git a/apps/assets/backends/base.py b/apps/assets/backends/base.py index d46c67f70..9c9f7ca1e 100644 --- a/apps/assets/backends/base.py +++ b/apps/assets/backends/base.py @@ -43,7 +43,7 @@ class AssetUserQuerySet(list): else: in_kwargs[k] = v for k in in_kwargs: - kwargs.pop(k) + kwargs.pop(k, None) if len(in_kwargs) == 0: return self @@ -56,7 +56,7 @@ class AssetUserQuerySet(list): v = [str(i) for i in v] if isinstance(attr, uuid.UUID): attr = str(attr) - if v in attr: + if attr in v: matched = True if matched: queryset.append(i) @@ -68,11 +68,12 @@ class AssetUserQuerySet(list): real = [] for k, v in kwargs.items(): wanted.append(v) - value = getattr(obj, k) + value = getattr(obj, k, None) if isinstance(value, uuid.UUID): value = str(value) real.append(value) return wanted == real + kwargs = {k: v for k, v in kwargs.items() if k.find('__in') == -1} if len(kwargs) > 0: queryset = AssetUserQuerySet([i for i in self if filter_it(i)]) else: diff --git a/apps/assets/forms/__init__.py b/apps/assets/forms/__init__.py index a086cb12c..39b39a45a 100644 --- a/apps/assets/forms/__init__.py +++ b/apps/assets/forms/__init__.py @@ -5,3 +5,4 @@ from .label import * from .user import * from .domain import * from .cmd_filter import * +from .platform import * diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index 805f49f24..46c137d70 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -6,13 +6,13 @@ from django.utils.translation import gettext_lazy as _ from common.utils import get_logger from orgs.mixins.forms import OrgModelForm -from ..models import Asset, Node +from ..models import Asset from ..const import GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT logger = get_logger(__file__) __all__ = [ - 'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm', + 'AssetCreateUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm', ] @@ -27,17 +27,27 @@ class ProtocolForm(forms.Form): ) -class AssetCreateForm(OrgModelForm): +class AssetCreateUpdateForm(OrgModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if self.data: - return + self.set_platform_to_name() + self.set_fields_queryset() + + def set_fields_queryset(self): nodes_field = self.fields['nodes'] + nodes_choices = [] if self.instance: - nodes_field.choices = [(n.id, n.full_value) for n in - self.instance.nodes.all()] - else: - nodes_field.choices = [] + nodes_choices = [ + (n.id, n.full_value) for n in + self.instance.nodes.all() + ] + nodes_field.choices = nodes_choices + + def set_platform_to_name(self): + platform_field = self.fields['platform'] + platform_field.to_field_name = 'name' + if self.instance: + self.initial['platform'] = self.instance.platform.name def add_nodes_initial(self, node): nodes_field = self.fields['nodes'] @@ -49,7 +59,7 @@ class AssetCreateForm(OrgModelForm): fields = [ 'hostname', 'ip', 'public_ip', 'protocols', 'comment', 'nodes', 'is_active', 'admin_user', 'labels', 'platform', - 'domain', + 'domain', 'number', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ @@ -64,52 +74,8 @@ class AssetCreateForm(OrgModelForm): 'domain': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Domain') }), - } - labels = { - 'nodes': _("Node"), - } - help_texts = { - 'hostname': GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT, - 'admin_user': _( - 'root or other NOPASSWD sudo privilege user existed in asset,' - 'If asset is windows or other set any one, more see admin user left menu' - ), - 'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"), - 'domain': _("If your have some network not connect with each other, you can set domain") - } - - -class AssetUpdateForm(OrgModelForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.data: - return - nodes_field = self.fields['nodes'] - if self.instance: - nodes_field.choices = ((n.id, n.full_value) for n in - self.instance.nodes.all()) - else: - nodes_field.choices = [] - - class Meta: - model = Asset - fields = [ - 'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform', - 'public_ip', 'number', 'comment', 'admin_user', 'labels', - 'domain', - ] - widgets = { - 'nodes': forms.SelectMultiple(attrs={ - 'class': 'nodes-select2', 'data-placeholder': _('Node') - }), - 'admin_user': forms.Select(attrs={ - 'class': 'select2', 'data-placeholder': _('Admin user') - }), - 'labels': forms.SelectMultiple(attrs={ - 'class': 'select2', 'data-placeholder': _('Label') - }), - 'domain': forms.Select(attrs={ - 'class': 'select2', 'data-placeholder': _('Domain') + 'platform': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Platform') }), } labels = { diff --git a/apps/assets/forms/platform.py b/apps/assets/forms/platform.py new file mode 100644 index 000000000..88c4365d4 --- /dev/null +++ b/apps/assets/forms/platform.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from ..models import Platform + + +__all__ = ['PlatformForm', 'PlatformMetaForm'] + + +class PlatformMetaForm(forms.Form): + SECURITY_CHOICES = ( + ('rdp', "RDP"), + ('nla', "NLA"), + ('tls', 'TLS'), + ('any', "Any"), + ) + CONSOLE_CHOICES = ( + (True, _('Yes')), + (False, _('No')), + ) + security = forms.ChoiceField( + choices=SECURITY_CHOICES, initial='any', label=_("RDP security"), + required=False, + ) + console = forms.ChoiceField( + choices=CONSOLE_CHOICES, initial=False, label=_("RDP console"), + required=False, + ) + + +class PlatformForm(forms.ModelForm): + class Meta: + model = Platform + fields = [ + 'name', 'base', 'comment', + ] + labels = { + 'base': _("Base platform") + } + diff --git a/apps/assets/migrations/0044_platform.py b/apps/assets/migrations/0044_platform.py new file mode 100644 index 000000000..8d45a8ee3 --- /dev/null +++ b/apps/assets/migrations/0044_platform.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2.7 on 2019-12-06 07:26 + +import common.fields.model +from django.db import migrations, models + + +def create_internal_platform(apps, schema_editor): + model = apps.get_model("assets", "Platform") + db_alias = schema_editor.connection.alias + type_platforms = ( + ('Linux', 'Linux', None), + ('Unix', 'Unix', None), + ('MacOS', 'MacOS', None), + ('BSD', 'BSD', None), + ('Windows', 'Windows', None), + ('Windows2016', 'Windows', {'security': 'tls'}), + ('Other', 'Other', None), + ) + for name, base, meta in type_platforms: + model.objects.using(db_alias).create( + name=name, base=base, internal=True, meta=meta + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0043_auto_20191114_1111'), + ] + + operations = [ + migrations.CreateModel( + name='Platform', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')), + ('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')), + ('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')), + ('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')), + ('internal', models.BooleanField(default=False, verbose_name='Internal')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), + ], + options={ + 'verbose_name': 'Platform' + } + ), + migrations.RunPython(create_internal_platform) + ] diff --git a/apps/assets/migrations/0045_auto_20191206_1607.py b/apps/assets/migrations/0045_auto_20191206_1607.py new file mode 100644 index 000000000..f51839289 --- /dev/null +++ b/apps/assets/migrations/0045_auto_20191206_1607.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.7 on 2019-12-06 08:07 + +import assets.models.asset +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_platform_to_asset_type(apps, schema_editor): + asset_model = apps.get_model("assets", "Asset") + platform_model = apps.get_model("assets", "Platform") + db_alias = schema_editor.connection.alias + + platforms = platform_model.objects.using(db_alias).all() + platforms_map = {p.name: p for p in platforms} + for name, p in platforms_map.items(): + asset_model.objects.using(db_alias)\ + .filter(_platform=name)\ + .update(platform=p) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0044_platform'), + ] + + operations = [ + migrations.RenameField( + model_name='asset', + old_name='platform', + new_name='_platform', + ), + migrations.AddField( + model_name='asset', + name='platform', + field=models.ForeignKey( + default=assets.models.asset.Platform.default, + on_delete=django.db.models.deletion.PROTECT, + related_name='assets', to='assets.Platform', + verbose_name='Platform'), + ), + migrations.RunPython(migrate_platform_to_asset_type), + migrations.RemoveField( + model_name='asset', + name='_platform', + ), + ] diff --git a/apps/assets/migrations/0046_auto_20191218_1705.py b/apps/assets/migrations/0046_auto_20191218_1705.py new file mode 100644 index 000000000..af776eee2 --- /dev/null +++ b/apps/assets/migrations/0046_auto_20191218_1705.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.11 on 2019-12-18 09:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0045_auto_20191206_1607'), + ] + + operations = [ + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + ] diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index d6e09786e..70e2abb2a 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -11,10 +11,12 @@ from collections import OrderedDict from django.db import models from django.utils.translation import ugettext_lazy as _ -from .utils import Connectivity +from common.fields.model import JsonDictTextField +from common.utils import lazyproperty from orgs.mixins.models import OrgModelMixin, OrgManager +from .utils import Connectivity -__all__ = ['Asset', 'ProtocolsMixin'] +__all__ = ['Asset', 'ProtocolsMixin', 'Platform'] logger = logging.getLogger(__name__) @@ -37,6 +39,13 @@ def default_node(): return None +class AssetManager(OrgManager): + def get_queryset(self): + return super().get_queryset().annotate( + platform_base=models.F('platform__base') + ) + + class AssetQuerySet(models.QuerySet): def active(self): return self.filter(is_active=True) @@ -119,6 +128,47 @@ class NodesRelationMixin: return nodes +class Platform(models.Model): + CHARSET_CHOICES = ( + ('utf8', 'UTF-8'), + ('gbk', 'GBK'), + ) + BASE_CHOICES = ( + ('Linux', 'Linux'), + ('Unix', 'Unix'), + ('MacOS', 'MacOS'), + ('BSD', 'BSD'), + ('Windows', 'Windows'), + ('Other', 'Other'), + ) + name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True) + base = models.CharField(choices=BASE_CHOICES, max_length=16, default='Linux', verbose_name=_("Base")) + charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset")) + meta = JsonDictTextField(blank=True, null=True, verbose_name=_("Meta")) + internal = models.BooleanField(default=False, verbose_name=_("Internal")) + comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) + + @classmethod + def default(cls): + linux, created = cls.objects.get_or_create( + defaults={'name': 'Linux'}, name='Linux' + ) + return linux.id + + def is_windows(self): + return self.base.lower() in ('windows',) + + def is_unixlike(self): + return self.base.lower() in ("linux", "unix", "macos", "bsd") + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Platform") + # ordering = ('name',) + + class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): # Important PLATFORM_CHOICES = ( @@ -138,9 +188,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): choices=ProtocolsMixin.PROTOCOL_CHOICES, verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) - protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) - platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) + platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) @@ -175,7 +224,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) - objects = OrgManager.from_queryset(AssetQuerySet)() + objects = AssetManager.from_queryset(AssetQuerySet)() _connectivity = None def __str__(self): @@ -190,20 +239,18 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): return False, warning return True, warning + @lazyproperty + def platform_base(self): + return self.platform.base + def is_windows(self): - if self.platform in ("Windows", "Windows2016"): - return True - else: - return False + return self.platform.is_windows() def is_unixlike(self): - if self.platform not in ("Windows", "Windows2016", "Other"): - return True - else: - return False + return self.platform.is_unixlike() def is_support_ansible(self): - return self.has_protocol('ssh') and self.platform not in ("Other",) + return self.has_protocol('ssh') and self.platform_base not in ("Other",) @property def cpu_info(self): @@ -264,9 +311,9 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): def as_tree_node(self, parent_node): from common.tree import TreeNode icon_skin = 'file' - if self.platform.lower() == 'windows': + if self.platform_base.lower() == 'windows': icon_skin = 'windows' - elif self.platform.lower() == 'linux': + elif self.platform_base.lower() == 'linux': icon_skin = 'linux' data = { 'id': str(self.id), @@ -283,7 +330,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): 'hostname': self.hostname, 'ip': self.ip, 'protocols': self.protocols_as_list, - 'platform': self.platform, + 'platform': self.platform_base, } } } diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 2e591c940..895e0134d 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -11,14 +11,13 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from common.utils import ( - get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger + signer, ssh_key_string_to_obj, ssh_key_gen, get_logger ) from common.validators import alphanumeric from common import fields from orgs.mixins.models import OrgModelMixin from .utils import private_key_validator, Connectivity -signer = get_signer() logger = get_logger(__file__) @@ -41,6 +40,7 @@ class AssetUser(OrgModelMixin): ASSET_USER_CACHE_TIME = 3600 * 24 _prefer = "system_user" + _assets_amount = None @property def private_key_obj(self): @@ -143,6 +143,8 @@ class AssetUser(OrgModelMixin): @property def assets_amount(self): + if self._assets_amount is not None: + return self._assets_amount cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) cached = cache.get(cache_key) if not cached: diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 443c32981..d3e3087ec 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -10,14 +10,13 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator -from common.utils import get_signer +from common.utils import signer from .base import AssetUser from .asset import Asset __all__ = ['AdminUser', 'SystemUser'] logger = logging.getLogger(__name__) -signer = get_signer() class AdminUser(AssetUser): @@ -93,11 +92,13 @@ class SystemUser(AssetUser): PROTOCOL_RDP = 'rdp' PROTOCOL_TELNET = 'telnet' PROTOCOL_VNC = 'vnc' + PROTOCOL_MYSQL = 'mysql' PROTOCOL_CHOICES = ( (PROTOCOL_SSH, 'ssh'), (PROTOCOL_RDP, 'rdp'), (PROTOCOL_TELNET, 'telnet'), (PROTOCOL_VNC, 'vnc'), + (PROTOCOL_MYSQL, 'mysql'), ) LOGIN_AUTO = 'auto' @@ -134,6 +135,18 @@ class SystemUser(AssetUser): else: return False + @property + def is_need_cmd_filter(self): + return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_MYSQL] + + @property + def is_need_test_asset_connective(self): + return self.protocol not in [self.PROTOCOL_MYSQL] + + @property + def can_perm_to_asset(self): + return self.protocol not in [self.PROTOCOL_MYSQL] + @property def cmd_filter_rules(self): from .cmd_filter import CommandFilterRule diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index a24374c13..6948f20e2 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.serializers import AdaptedBulkListSerializer -from ..models import Asset, Node, Label +from ..models import Asset, Node, Label, Platform from ..const import ( GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN, GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG @@ -16,7 +16,8 @@ from .base import ConnectivitySerializer __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', - 'ProtocolsField', + 'ProtocolsField', 'PlatformSerializer', + 'AssetDetailSerializer', ] @@ -65,6 +66,9 @@ class ProtocolsField(serializers.ListField): class AssetSerializer(BulkOrgResourceModelSerializer): + platform = serializers.SlugRelatedField( + slug_field='name', queryset=Platform.objects.all(), label=_("Platform") + ) protocols = ProtocolsField(label=_('Protocols'), required=False) connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) @@ -111,7 +115,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer): queryset = queryset.prefetch_related( Prefetch('nodes', queryset=Node.objects.all().only('id')), Prefetch('labels', queryset=Label.objects.all().only('id')), - ).select_related('admin_user', 'domain') + ).select_related('admin_user', 'domain', 'platform') return queryset def compatible_with_old_protocol(self, validated_data): @@ -139,6 +143,21 @@ class AssetSerializer(BulkOrgResourceModelSerializer): return super().update(instance, validated_data) +class PlatformSerializer(serializers.ModelSerializer): + meta = serializers.DictField(required=False, allow_null=True) + + class Meta: + model = Platform + fields = [ + 'id', 'name', 'base', 'charset', + 'internal', 'meta', 'comment' + ] + + +class AssetDetailSerializer(AssetSerializer): + platform = PlatformSerializer(read_only=True) + + class AssetSimpleSerializer(serializers.ModelSerializer): connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 648af01a7..79cc58897 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -4,8 +4,10 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from common.serializers import AdaptedBulkListSerializer +from common.mixins.serializers import BulkSerializerMixin from common.utils import ssh_pubkey_gen from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from assets.models import Node from ..models import SystemUser from ..const import ( GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN, @@ -13,6 +15,12 @@ from ..const import ( ) from .base import AuthSerializer, AuthSerializerMixin +__all__ = [ + 'SystemUserSerializer', 'SystemUserAuthSerializer', + 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', + 'SystemUserNodeRelationSerializer', +] + class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): """ @@ -95,8 +103,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): def validate(self, attrs): username = attrs.get("username", "manual") + auto_gen_key = attrs.pop("auto_generate_key", False) protocol = attrs.get("protocol") - auto_gen_key = attrs.get("auto_generate_key", False) + + if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]: + return attrs + if auto_gen_key: password = SystemUser.gen_password() attrs["password"] = password @@ -111,7 +123,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): public_key = ssh_pubkey_gen(private_key, password=password, username=username) attrs["public_key"] = public_key - attrs.pop("auto_generate_key", None) return attrs @classmethod @@ -143,4 +154,43 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'username') +class RelationMixin(BulkSerializerMixin, serializers.Serializer): + systemuser_display = serializers.ReadOnlyField() + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(['systemuser', "systemuser_display"]) + return fields + + class Meta: + list_serializer_class = AdaptedBulkListSerializer + + +class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): + asset_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = SystemUser.assets.through + fields = [ + 'id', "asset", "asset_display", + ] + + +class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): + node_display = serializers.SerializerMethodField() + + class Meta(RelationMixin.Meta): + model = SystemUser.nodes.through + fields = [ + 'id', 'node', "node_display", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tree = Node.tree() + + def get_node_display(self, obj): + if hasattr(obj, 'node_key'): + return self.tree.get_node_full_tag(obj.node_key) + else: + return obj.node.full_value diff --git a/apps/assets/templates/assets/_admin_user_import_modal.html b/apps/assets/templates/assets/_admin_user_import_modal.html deleted file mode 100644 index a4afc1a14..000000000 --- a/apps/assets/templates/assets/_admin_user_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import admin user" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-assets:admin-user-list" %}{% endblock %} diff --git a/apps/assets/templates/assets/_admin_user_update_modal.html b/apps/assets/templates/assets/_admin_user_update_modal.html deleted file mode 100644 index 9af051dd2..000000000 --- a/apps/assets/templates/assets/_admin_user_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update admin user" %}{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/_asset_import_modal.html b/apps/assets/templates/assets/_asset_import_modal.html deleted file mode 100644 index 2460cb053..000000000 --- a/apps/assets/templates/assets/_asset_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import assets" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %} diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html index 7c3e03f17..dea2c3e1e 100644 --- a/apps/assets/templates/assets/_asset_list_modal.html +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -25,7 +25,7 @@
-
+
@@ -37,7 +37,7 @@
-
+
@@ -190,6 +190,16 @@ function setAssetModalOptions(options) { assetModalOption = options; } +function initAssetTreeModel(selector) { + $(selector).parent().find(".select2-selection").on('click', function (e) { + if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){ + e.preventDefault(); + e.stopPropagation(); + $("#asset_list_modal").modal(); + } + }) +} + $(document).ready(function(){ diff --git a/apps/assets/templates/assets/_asset_update_modal.html b/apps/assets/templates/assets/_asset_update_modal.html deleted file mode 100644 index 68b2ff8db..000000000 --- a/apps/assets/templates/assets/_asset_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update assets" %}{% endblock %} diff --git a/apps/assets/templates/assets/_node_detail_modal.html b/apps/assets/templates/assets/_node_detail_modal.html new file mode 100644 index 000000000..f1f6f2dda --- /dev/null +++ b/apps/assets/templates/assets/_node_detail_modal.html @@ -0,0 +1,68 @@ +{% extends '_modal.html' %} +{% load i18n %} +{% load static %} + +{% block modal_id %}node_detail_modal{% endblock %} + +{% block modal_title %}{% trans "Node detail" %}{% endblock %} + + +{% block modal_body %} + +
+
+ +
+

+
+
+ +
+
+
+ +
+

+
+
+
+ +
+

+
+
+
+ +
+

+
+
+
+ + + + +{% endblock %} + +{% block modal_button %} + +{% endblock %} diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html index fc7f4ae06..b4b5040b0 100644 --- a/apps/assets/templates/assets/_node_tree.html +++ b/apps/assets/templates/assets/_node_tree.html @@ -216,8 +216,9 @@ function OnRightClick(event, treeId, treeNode) { function showRMenu(type, x, y) { var offset = $("#tree-node-id").offset(); + var scrollTop = document.querySelector('.treebox').scrollTop; x -= offset.left; - y -= offset.top; + y -= offset.top + scrollTop; x += document.body.scrollLeft; y += document.body.scrollTop + document.documentElement.scrollTop; rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"}); diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index 078877cfa..c3cba50e7 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
@@ -99,7 +95,7 @@ function autoLoginModeProtocol() { // 协议+自动登录模式字段控制 $('#auth_title_id').removeClass('hidden'); var protocol = $(protocol_id + " option:selected").text(); - if (protocol === 'rdp') { + if (['rdp'].indexOf(protocol) !== -1) { authFieldsDisplay(); $(auto_generate_key).closest('.form-group').removeClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden'); @@ -109,7 +105,7 @@ function autoLoginModeProtocol() { $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } - else if (protocol === 'vnc') { + else if (['vnc', 'mysql'].indexOf(protocol) !== -1) { $('.auth-fields').removeClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden'); @@ -145,7 +141,7 @@ function manualLoginModeProtocol() { // 协议+手动登录模式字段控制 $('#auth_title_id').addClass('hidden'); var protocol = $(protocol_id + " option:selected").text(); - if (protocol === 'rdp') { + if (['rdp'].indexOf(protocol) !== -1) { $('.auth-fields').addClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').addClass('hidden'); @@ -155,7 +151,7 @@ function manualLoginModeProtocol() { $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } - else if (protocol === 'vnc') { + else if (['vnc', 'mysql'].indexOf(protocol) !== -1) { $('.auth-fields').addClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').addClass('hidden'); @@ -247,4 +243,4 @@ $(document).ready(function () { }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/assets/templates/assets/_system_user_import_modal.html b/apps/assets/templates/assets/_system_user_import_modal.html deleted file mode 100644 index b8687d696..000000000 --- a/apps/assets/templates/assets/_system_user_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import system user" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-assets:system-user-list" %}{% endblock %} diff --git a/apps/assets/templates/assets/_system_user_update_modal.html b/apps/assets/templates/assets/_system_user_update_modal.html deleted file mode 100644 index 9e2920e6a..000000000 --- a/apps/assets/templates/assets/_system_user_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update system user" %}{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index a2a52e0e2..e77cbb689 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
diff --git a/apps/assets/templates/assets/admin_user_create_update.html b/apps/assets/templates/assets/admin_user_create_update.html index c94a67a9e..213f038d1 100644 --- a/apps/assets/templates/assets/admin_user_create_update.html +++ b/apps/assets/templates/assets/admin_user_create_update.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
@@ -87,4 +83,4 @@ $(document).ready(function () { }) }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/assets/templates/assets/admin_user_detail.html b/apps/assets/templates/assets/admin_user_detail.html index 66480a2cb..e3f618d93 100644 --- a/apps/assets/templates/assets/admin_user_detail.html +++ b/apps/assets/templates/assets/admin_user_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html index 8063d6de6..93477474c 100644 --- a/apps/assets/templates/assets/admin_user_list.html +++ b/apps/assets/templates/assets/admin_user_list.html @@ -5,28 +5,7 @@ {% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%} {% endblock %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} @@ -42,9 +21,6 @@
-{# #} -{# #} -{# #} @@ -52,8 +28,6 @@
{% trans 'Name' %} {% trans 'Username' %} {% trans 'Asset' %}{% trans 'Reachable' %}{% trans 'Unreachable' %}{% trans 'Ratio' %}{% trans 'Comment' %} {% trans 'Action' %}
- {% include 'assets/_admin_user_import_modal.html' %} - {% include 'assets/_admin_user_update_modal.html' %} {% endblock %} {% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} @@ -77,17 +51,16 @@ function initTable() { columns: [ {data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount", orderable: false}, {#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#} - {data: "comment"}, {data: "id", orderable: false, width: "100px"} + {data: "comment"}, {data: "id", orderable: false, width: "120px"} ] }; - admin_user_table = jumpserver.initServerSideDataTable(options); - return admin_user_table + return jumpserver.initServerSideDataTable(options); } $(document).ready(function(){ - initTable(); + admin_user_table = initTable(); + initCsvImportExport(admin_user_table, "{% trans "Admin user" %}") }) - .on('click', '.btn_admin_user_delete', function () { var $this = $(this); var $data_table = $("#admin_user_list_table").DataTable(); @@ -100,69 +73,5 @@ $(document).ready(function(){ }, 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 - }); -}) {% endblock %} diff --git a/apps/assets/templates/assets/asset_bulk_update.html b/apps/assets/templates/assets/asset_bulk_update.html index 08df48e91..207d5eb52 100644 --- a/apps/assets/templates/assets/asset_bulk_update.html +++ b/apps/assets/templates/assets/asset_bulk_update.html @@ -32,13 +32,7 @@ {% endblock %} diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index d52532848..2b990619c 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -1,132 +1,54 @@ -{% extends 'base.html' %} +{% extends '_base_asset_tree_list.html' %} {% load static %} {% load i18n %} {% block help_message %} -{#
#} -{# #} -{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#} {% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %} -{#
#} {% endblock %} -{% block custom_head_css_js %} -{# #} -{# #} - - - -{% endblock %} - -{% block content %} -
-
-
- {% include 'assets/_node_tree.html' %} -
-
-
-
- -
-
-
- - -
- - -
- - - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Hardware' %}{% trans 'Reachable' %}{% trans 'Action' %}
-
-
- -
- -
-
-
+{% block table_container %} + + {% include '_csv_import_export.html' %} +
+ + +
+ + + + + + + + + + + + + +
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Hardware' %}{% trans 'Reachable' %}{% trans 'Action' %}
+
+
+ +
+
-
- -{% include 'assets/_asset_update_modal.html' %} -{% include 'assets/_asset_import_modal.html' %} {% include 'assets/_asset_list_modal.html' %} +{% include 'assets/_node_detail_modal.html' %} {% endblock %} {% block custom_foot_js %} @@ -177,7 +99,7 @@ function initTable() { data: "connectivity", orderable: false, width: '60px' - }, {data: "id", orderable: false, width: "100px"} + }, {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; @@ -198,26 +120,12 @@ function initTree() {
  • +
  • + ` }) } - -function toggle() { - if (show === 0) { - $("#split-left").hide(500, function () { - $("#split-right").attr("class", "col-lg-12"); - $("#toggle-icon").attr("class", "fa fa-angle-right fa-x"); - show = 1; - }); - } else { - $("#split-right").attr("class", "col-lg-9"); - $("#toggle-icon").attr("class", "fa fa-angle-left fa-x"); - $("#split-left").show(500); - show = 0; - } -} - function onNodeSelected(event, treeNode) { current_node = treeNode; current_node_id = treeNode.meta.node.id; @@ -258,7 +166,8 @@ function onAssetModalConfirmAddAssetToNode(table) { } $(document).ready(function(){ - initTable(); + asset_table = initTable(); + initCsvImportExport(asset_table, "{% trans "Asset" %}"); initTree(); if(getCookie('show_current_asset') === '1'){ @@ -279,81 +188,6 @@ $(document).ready(function(){ $("#asset_list_table_filter input").val(val); asset_table.search(val).draw(); }) -.on('click', '.btn_export', function () { - var assets = asset_table.selected; - var data = { - 'resources': assets - }; - var search = $("input[type='search']").val(); - var props = { - method: "POST", - body: JSON.stringify(data), - success_url: "{% url 'api-assets:asset-list' %}", - format: 'csv', - params: { - search: search, - node_id: current_node_id || '', - show_current_asset: getCookie('show_current_asset') - } - }; - APIExportData(props); -}) -.on('click', '#btn_import_confirm', function () { - var file = document.getElementById('id_file').files[0]; - if(!file){ - toastr.error("{% trans "Please select file" %}"); - return - } - var url = "{% url 'api-assets:asset-list' %}"; - if (current_node_id){ - url = setUrlParam(url, 'node_id', current_node_id); - } - var data_table = $('#asset_list_table').DataTable(); - - APIImportData({ - url: url, - method: "POST", - body: file, - data_table: data_table - }); -}) -.on('click', '#download_update_template', function () { - var assets = asset_table.selected; - var data = { - 'resources': assets - }; - var search = $("input[type='search']").val(); - var props = { - method: "POST", - body: JSON.stringify(data), - success_url: "{% url 'api-assets:asset-list' %}?format=csv&template=update", - format: 'csv', - params: { - search: search, - node_id: current_node_id || '' - } - }; - APIExportData(props); -}) -.on('click', '#btn_update_confirm', function () { - var file = document.getElementById('update_file').files[0]; - if(!file){ - toastr.error("{% trans "Please select file" %}"); - return - } - var url = "{% url 'api-assets:asset-list' %}"; - if (current_node_id){ - url = setUrlParam(url, 'node_id', current_node_id); - } - var data_table = $('#asset_list_table').DataTable(); - - APIImportData({ - url: url, - method: "PUT", - body: file, - data_table: data_table - }); -}) .on('click', '.btn-create-asset', function () { var url = "{% url 'assets:asset-create' %}"; if (current_node_id) { @@ -382,9 +216,6 @@ $(document).ready(function(){ var data = { 'resources': id_list }; - function refreshPage() { - setTimeout( function () {window.location.reload();}, 300); - } function reloadTable() { asset_table.ajax.reload(); @@ -550,6 +381,30 @@ $(document).ready(function(){ flash_message: false }); +}).on('click', '#menu_node_detail', function(e) { + e.preventDefault(); + var the_url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}"; + the_url = the_url.replace("{{ DEFAULT_PK }}", current_node_id); + function drawingNodeDetailModal(data){ + $('#id_node_detail_id_view').html(data['id']); + $('#id_node_detail_name_view').html(data['name']); + $('#id_node_detail_full_name_view').html(data['full_value']); + $('#id_node_detail_key_view').html(data['key']); + $('#node_detail_modal').modal(); + } + function error(data) { + alert(data) + } + function success(data) { + drawingNodeDetailModal(data) + } + requestApi({ + url: the_url, + error: error, + method: 'GET', + success: success, + flash_message: false + }); }) diff --git a/apps/assets/templates/assets/cmd_filter_detail.html b/apps/assets/templates/assets/cmd_filter_detail.html index 8ef5d5b75..24e192253 100644 --- a/apps/assets/templates/assets/cmd_filter_detail.html +++ b/apps/assets/templates/assets/cmd_filter_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    diff --git a/apps/assets/templates/assets/cmd_filter_list.html b/apps/assets/templates/assets/cmd_filter_list.html index 658c8266f..1d98d5500 100644 --- a/apps/assets/templates/assets/cmd_filter_list.html +++ b/apps/assets/templates/assets/cmd_filter_list.html @@ -62,7 +62,7 @@ function initTable() { columns: [ {data: "id"}, {data: "name" }, {data: "rules", orderable: false}, {data: "system_users", orderable: false}, {data: "comment"}, - {data: "id", orderable: false, width: "100px"} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/apps/assets/templates/assets/cmd_filter_rule_create_update.html b/apps/assets/templates/assets/cmd_filter_rule_create_update.html index 2edaa97dc..21279b410 100644 --- a/apps/assets/templates/assets/cmd_filter_rule_create_update.html +++ b/apps/assets/templates/assets/cmd_filter_rule_create_update.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -91,4 +87,4 @@ $(document).ready(function(){ formSubmit(props); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/assets/templates/assets/cmd_filter_rule_list.html b/apps/assets/templates/assets/cmd_filter_rule_list.html index 4bdf5ff2b..6076cb2ac 100644 --- a/apps/assets/templates/assets/cmd_filter_rule_list.html +++ b/apps/assets/templates/assets/cmd_filter_rule_list.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    diff --git a/apps/assets/templates/assets/domain_create_update.html b/apps/assets/templates/assets/domain_create_update.html index 96bfd02aa..39939c8ca 100644 --- a/apps/assets/templates/assets/domain_create_update.html +++ b/apps/assets/templates/assets/domain_create_update.html @@ -25,9 +25,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/assets/templates/assets/domain_detail.html b/apps/assets/templates/assets/domain_detail.html index c05e0ed80..5b01c30fe 100644 --- a/apps/assets/templates/assets/domain_detail.html +++ b/apps/assets/templates/assets/domain_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -138,4 +133,4 @@ $(document).ready(function(){ }) ; -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/assets/templates/assets/domain_gateway_list.html b/apps/assets/templates/assets/domain_gateway_list.html index 79636a2bc..8917ef810 100644 --- a/apps/assets/templates/assets/domain_gateway_list.html +++ b/apps/assets/templates/assets/domain_gateway_list.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    diff --git a/apps/assets/templates/assets/domain_list.html b/apps/assets/templates/assets/domain_list.html index e9fb528e1..623f1bea2 100644 --- a/apps/assets/templates/assets/domain_list.html +++ b/apps/assets/templates/assets/domain_list.html @@ -55,7 +55,7 @@ function initTable() { ajax_url: '{% url "api-assets:domain-list" %}', columns: [ {data: "id"}, {data: "name" }, {data: "asset_count", orderable: false }, - {data: "gateway_count", orderable: false }, {data: "comment" }, {data: "id", orderable: false, width: "100px"} + {data: "gateway_count", orderable: false }, {data: "comment" }, {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/apps/assets/templates/assets/gateway_create_update.html b/apps/assets/templates/assets/gateway_create_update.html index 315302b6f..43dc07b1b 100644 --- a/apps/assets/templates/assets/gateway_create_update.html +++ b/apps/assets/templates/assets/gateway_create_update.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -125,4 +121,4 @@ $(document).ready(function(){ protocolChange(); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/assets/templates/assets/label_create_update.html b/apps/assets/templates/assets/label_create_update.html index 41646003c..62732d6ba 100644 --- a/apps/assets/templates/assets/label_create_update.html +++ b/apps/assets/templates/assets/label_create_update.html @@ -29,9 +29,7 @@ $(document).ready(function () { $('.select2').select2({ closeOnSelect: false }) -}).on('click', '.select2-selection__rendered', function (e) { - e.preventDefault(); - $("#asset_list_modal").modal(); + initAssetTreeModel("#id_assets"); }) .on("submit", "form", function (evt) { evt.preventDefault(); @@ -55,4 +53,4 @@ $(document).ready(function () { formSubmit(props); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/assets/templates/assets/label_list.html b/apps/assets/templates/assets/label_list.html index 9ab735f7a..104e5820b 100644 --- a/apps/assets/templates/assets/label_list.html +++ b/apps/assets/templates/assets/label_list.html @@ -45,7 +45,7 @@ function initTable() { columns: [ {data: "id"}, {data: "name" }, {data: "value" }, {data: "asset_count", orderable: false}, - {data: "id", orderable: false, width: "100px"} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/apps/assets/templates/assets/platform_create_update.html b/apps/assets/templates/assets/platform_create_update.html new file mode 100644 index 000000000..e130e9e97 --- /dev/null +++ b/apps/assets/templates/assets/platform_create_update.html @@ -0,0 +1,79 @@ +{% extends '_base_create_update.html' %} {% load static %} {% load bootstrap3 %} +{% load i18n %} + +{% block form %} +
    + {% csrf_token %} + {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.base layout="horizontal" %} +
    + +
    + {% bootstrap_field form.comment layout="horizontal" %} + +
    +
    +
    + + +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} + diff --git a/apps/assets/templates/assets/platform_detail.html b/apps/assets/templates/assets/platform_detail.html new file mode 100644 index 000000000..86d7f93e1 --- /dev/null +++ b/apps/assets/templates/assets/platform_detail.html @@ -0,0 +1,75 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'Base platform' %}:{{ object.base }}
    {% trans 'Charset' %}:{{ object.charset }}
    {% trans 'Meta' %}:{{ object.meta }}
    {% trans 'Comment' %}:{{ object.comment }}
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} +{% endblock %} diff --git a/apps/assets/templates/assets/platform_list.html b/apps/assets/templates/assets/platform_list.html new file mode 100644 index 000000000..cb1eef5bc --- /dev/null +++ b/apps/assets/templates/assets/platform_list.html @@ -0,0 +1,75 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block table_search %} +{% endblock %} + +{% block table_container %} + + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'Base platform' %}{% trans 'Comment' %}{% trans 'Action' %}
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/assets/templates/assets/system_user_assets.html b/apps/assets/templates/assets/system_user_assets.html index 4229d12ab..ec024c1a7 100644 --- a/apps/assets/templates/assets/system_user_assets.html +++ b/apps/assets/templates/assets/system_user_assets.html @@ -4,9 +4,13 @@ {% load i18n %} {% block custom_head_css_js %} - - + {% endblock %} + {% block content %}
    @@ -98,15 +102,8 @@ - - {% for node in system_user.nodes.all|sort %} - - {{ node.full_value }} - - - - - {% endfor %} + +
    @@ -120,91 +117,115 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index 9c0557fde..b9d6e5c0b 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -17,11 +12,13 @@
  • {% trans 'Detail' %}
  • + {% if system_user.can_perm_to_asset %}
  • {% trans 'Assets' %}
  • + {% endif %}
  • {% trans 'Update' %}
  • @@ -144,6 +141,7 @@ {% endif %} + {% if system_user.is_need_test_asset_connective %} {% trans 'Test assets connective' %}: @@ -152,13 +150,14 @@ + {% endif %}
    - {% if system_user.protocol != 'rdp' %} + {% if system_user.is_need_cmd_filter %}
    diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index be3f606d2..0e88b6461 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -8,28 +8,7 @@ {% endblock %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} @@ -57,8 +36,6 @@ - {% include 'assets/_system_user_import_modal.html' %} - {% include 'assets/_system_user_update_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 35651429b..653b71447 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -12,6 +12,7 @@ app_name = 'assets' router = BulkRouter() router.register(r'assets', api.AssetViewSet, 'asset') +router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'admin-users', api.AdminUserViewSet, 'admin-user') router.register(r'system-users', api.SystemUserViewSet, 'system-user') router.register(r'labels', api.LabelViewSet, 'label') @@ -23,6 +24,8 @@ router.register(r'asset-users', api.AssetUserViewSet, 'asset-user') router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') +router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') +router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') @@ -35,6 +38,8 @@ urlpatterns = [ api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), path('assets//gateway/', api.AssetGatewayApi.as_view(), name='asset-gateway'), + path('assets//platform/', + api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'), path('asset-users/auth-info/', api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'), diff --git a/apps/assets/urls/views_urls.py b/apps/assets/urls/views_urls.py index 71483a4df..eec9dcc0d 100644 --- a/apps/assets/urls/views_urls.py +++ b/apps/assets/urls/views_urls.py @@ -16,6 +16,11 @@ urlpatterns = [ # Asset user view path('asset//asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'), + path('platform/', views.PlatformListView.as_view(), name='platform-list'), + path('platform/create/', views.PlatformCreateView.as_view(), name='platform-create'), + path('platform//', views.PlatformDetailView.as_view(), name='platform-detail'), + path('platform//update/', views.PlatformUpdateView.as_view(), name='platform-update'), + # User asset view path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'), diff --git a/apps/assets/views/__init__.py b/apps/assets/views/__init__.py index 04fc6c31c..74055a76c 100644 --- a/apps/assets/views/__init__.py +++ b/apps/assets/views/__init__.py @@ -1,5 +1,6 @@ # coding:utf-8 from .asset import * +from .platform import * from .system_user import * from .admin_user import * from .label import * diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index f08c08db8..63179f4f8 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -74,7 +74,7 @@ class UserAssetListView(PermissionsMixin, TemplateView): class AssetCreateView(PermissionsMixin, FormMixin, TemplateView): model = Asset - form_class = forms.AssetCreateForm + form_class = forms.AssetCreateUpdateForm template_name = 'assets/asset_create.html' success_url = reverse_lazy('assets:asset-list') permission_classes = [IsOrgAdmin] @@ -110,7 +110,7 @@ class AssetCreateView(PermissionsMixin, FormMixin, TemplateView): class AssetUpdateView(PermissionsMixin, UpdateView): model = Asset - form_class = forms.AssetUpdateForm + form_class = forms.AssetCreateUpdateForm template_name = 'assets/asset_update.html' success_url = reverse_lazy('assets:asset-list') permission_classes = [IsOrgAdmin] diff --git a/apps/assets/views/domain.py b/apps/assets/views/domain.py index 67626b094..ad7fad1b6 100644 --- a/apps/assets/views/domain.py +++ b/apps/assets/views/domain.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # -from django.views.generic import TemplateView, CreateView, \ - UpdateView, DeleteView, DetailView +from django.views.generic import ( + TemplateView, CreateView, UpdateView, DeleteView, DetailView +) from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext_lazy as _ from django.urls import reverse_lazy, reverse diff --git a/apps/assets/views/platform.py b/apps/assets/views/platform.py new file mode 100644 index 000000000..2e3aff49c --- /dev/null +++ b/apps/assets/views/platform.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +from django.views import generic +from django.utils.translation import ugettext as _ + +from common.permissions import PermissionsMixin, IsSuperUser +from ..models import Platform +from ..forms import PlatformForm, PlatformMetaForm + +__all__ = [ + 'PlatformListView', 'PlatformUpdateView', 'PlatformCreateView', + 'PlatformDetailView', +] + + +class PlatformListView(PermissionsMixin, generic.TemplateView): + template_name = 'assets/platform_list.html' + permission_classes = (IsSuperUser,) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'app': _('Assets'), + 'action': _("Platform list"), + }) + return context + + +class PlatformCreateView(PermissionsMixin, generic.CreateView): + form_class = PlatformForm + permission_classes = (IsSuperUser,) + template_name = 'assets/platform_create_update.html' + model = Platform + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + meta_form = PlatformMetaForm() + context.update({ + 'app': _('Assets'), + 'action': _("Create platform"), + 'meta_form': meta_form, + }) + return context + + +class PlatformUpdateView(generic.UpdateView): + form_class = PlatformForm + permission_classes = (IsSuperUser,) + model = Platform + template_name = 'assets/platform_create_update.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + meta_form = PlatformMetaForm(initial=self.object.meta) + context.update({ + 'app': _('Assets'), + 'action': _("Update platform"), + 'type': 'update', + 'meta_form': meta_form, + }) + return context + + +class PlatformDetailView(generic.DetailView): + permission_classes = (IsSuperUser,) + model = Platform + template_name = 'assets/platform_detail.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'app': _('Assets'), + 'action': _("Platform detail"), + }) + return context diff --git a/apps/audits/api.py b/apps/audits/api.py index 4d7165b4b..3677a8e8e 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -11,4 +11,4 @@ class FTPLogViewSet(OrgModelViewSet): model = FTPLog serializer_class = FTPLogSerializer permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,) - + http_method_names = ['get', 'post', 'head', 'options'] diff --git a/apps/audits/migrations/0007_auto_20191202_1010.py b/apps/audits/migrations/0007_auto_20191202_1010.py new file mode 100644 index 000000000..3c355ff69 --- /dev/null +++ b/apps/audits/migrations/0007_auto_20191202_1010.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.7 on 2019-12-02 02:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0006_auto_20190726_1753'), + ] + + operations = [ + migrations.AlterField( + model_name='ftplog', + name='remote_addr', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'), + ), + migrations.AlterField( + model_name='operatelog', + name='remote_addr', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'), + ), + migrations.AlterField( + model_name='passwordchangelog', + name='remote_addr', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'), + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index eb3489a7c..81866bb1d 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -16,7 +16,7 @@ __all__ = [ class FTPLog(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=128, verbose_name=_('User')) - remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) asset = models.CharField(max_length=1024, verbose_name=_("Asset")) system_user = models.CharField(max_length=128, verbose_name=_("System user")) operate = models.CharField(max_length=16, verbose_name=_("Operate")) @@ -39,7 +39,7 @@ class OperateLog(OrgModelMixin): action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action")) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource = models.CharField(max_length=128, verbose_name=_("Resource")) - remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) datetime = models.DateTimeField(auto_now=True) def __str__(self): @@ -50,7 +50,7 @@ class PasswordChangeLog(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=128, verbose_name=_('User')) change_by = models.CharField(max_length=128, verbose_name=_("Change by")) - remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) datetime = models.DateTimeField(auto_now=True) def __str__(self): diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index 95ce8c05b..9579ca6e5 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -11,14 +11,15 @@ from rest_framework.request import Request from jumpserver.utils import current_request from common.utils import get_request_ip, get_logger, get_syslogger from users.models import User +from users.signals import post_user_change_password from authentication.signals import post_auth_failed, post_auth_success from terminal.models import Session, Command -from terminal.backends.command.serializers import SessionCommandSerializer -from . import models, serializers -from .tasks import write_login_log_async +from common.utils.encode import model_to_json +from .utils import write_login_log +from . import models logger = get_logger(__name__) -sys_logger = get_syslogger("audits") +sys_logger = get_syslogger(__name__) json_render = JSONRenderer() @@ -26,6 +27,8 @@ MODELS_NEED_RECORD = ( 'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter', 'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask', + 'Platform', 'ChangeAuthPlan', 'GatherUserTask', + 'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission', ) @@ -50,8 +53,11 @@ def create_operate_log(action, sender, resource): logger.error("Create operate log error: {}".format(e)) -@receiver(post_save, dispatch_uid="my_unique_identifier") -def on_object_created_or_update(sender, instance=None, created=False, **kwargs): +@receiver(post_save) +def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs): + if instance._meta.object_name == 'User' and \ + update_fields and 'last_login' in update_fields: + return if created: action = models.OperateLog.ACTION_CREATE else: @@ -59,47 +65,46 @@ def on_object_created_or_update(sender, instance=None, created=False, **kwargs): create_operate_log(action, sender, instance) -@receiver(post_delete, dispatch_uid="my_unique_identifier") +@receiver(post_delete) def on_object_delete(sender, instance=None, **kwargs): create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) -@receiver(post_save, sender=User, dispatch_uid="my_unique_identifier") -def on_user_change_password(sender, instance=None, **kwargs): - if hasattr(instance, '_set_password'): - if not current_request or not current_request.user.is_authenticated: - return - with transaction.atomic(): - models.PasswordChangeLog.objects.create( - user=instance, change_by=current_request.user, - remote_addr=get_request_ip(current_request), - ) +@receiver(post_user_change_password, sender=User) +def on_user_change_password(sender, user=None, **kwargs): + if not current_request: + remote_addr = '127.0.0.1' + change_by = 'System' + else: + remote_addr = get_request_ip(current_request) + if not current_request.user.is_authenticated: + change_by = str(user) + else: + change_by = str(current_request.user) + with transaction.atomic(): + models.PasswordChangeLog.objects.create( + user=str(user), change_by=change_by, + remote_addr=remote_addr, + ) def on_audits_log_create(sender, instance=None, **kwargs): if sender == models.UserLoginLog: category = "login_log" - serializer = serializers.LoginLogSerializer elif sender == models.FTPLog: - serializer = serializers.FTPLogSerializer category = "ftp_log" elif sender == models.OperateLog: category = "operation_log" - serializer = serializers.OperateLogSerializer elif sender == models.PasswordChangeLog: category = "password_change_log" - serializer = serializers.PasswordChangeLogSerializer elif sender == Session: category = "host_session_log" - serializer = serializers.SessionAuditSerializer elif sender == Command: category = "session_command_log" - serializer = SessionCommandSerializer else: return - s = serializer(instance=instance) - data = json_render.render(s.data).decode(errors='ignore') + data = model_to_json(instance, indent=None) msg = "{} - {}".format(category, data) sys_logger.info(msg) @@ -129,7 +134,7 @@ def on_user_auth_success(sender, user, request, **kwargs): logger.debug('User login success: {}'.format(user.username)) data = generate_data(user.username, request) data.update({'mfa': int(user.mfa_enabled), 'status': True}) - write_login_log_async.delay(**data) + write_login_log(**data) @receiver(post_auth_failed) @@ -137,4 +142,4 @@ def on_user_auth_failed(sender, username, request, reason, **kwargs): logger.debug('User login failed: {}'.format(username)) data = generate_data(username, request) data.update({'reason': reason, 'status': False}) - write_login_log_async.delay(**data) + write_login_log(**data) diff --git a/apps/audits/tasks.py b/apps/audits/tasks.py index 5f8da0bc0..2dfe4a2dd 100644 --- a/apps/audits/tasks.py +++ b/apps/audits/tasks.py @@ -6,8 +6,7 @@ from django.conf import settings from celery import shared_task from ops.celery.decorator import register_as_period_task -from .models import UserLoginLog -from .utils import write_login_log +from .models import UserLoginLog, OperateLog @register_as_period_task(interval=3600*24) @@ -22,6 +21,13 @@ def clean_login_log_period(): UserLoginLog.objects.filter(datetime__lt=expired_day).delete() +@register_as_period_task(interval=3600*24) @shared_task -def write_login_log_async(*args, **kwargs): - write_login_log(*args, **kwargs) +def clean_operation_log_period(): + now = timezone.now() + try: + days = int(settings.LOGIN_LOG_KEEP_DAYS) + except ValueError: + days = 90 + expired_day = now - datetime.timedelta(days=days) + OperateLog.objects.filter(datetime__lt=expired_day).delete() diff --git a/apps/audits/templates/audits/ftp_log_list.html b/apps/audits/templates/audits/ftp_log_list.html index 9e92f8481..e0399ca73 100644 --- a/apps/audits/templates/audits/ftp_log_list.html +++ b/apps/audits/templates/audits/ftp_log_list.html @@ -5,8 +5,6 @@ {% load common_tags %} {% block custom_head_css_js %} - - {% endblock %} diff --git a/apps/audits/templates/audits/login_log_list.html b/apps/audits/templates/audits/login_log_list.html index 0533f8aef..1ac74d311 100644 --- a/apps/audits/templates/audits/login_log_list.html +++ b/apps/audits/templates/audits/login_log_list.html @@ -102,47 +102,47 @@ {% block custom_foot_js %} - - + + }) + {% endblock %} diff --git a/apps/audits/templates/audits/operate_log_list.html b/apps/audits/templates/audits/operate_log_list.html index 31e219a85..fdc03ce35 100644 --- a/apps/audits/templates/audits/operate_log_list.html +++ b/apps/audits/templates/audits/operate_log_list.html @@ -5,8 +5,6 @@ {% load common_tags %} {% block custom_head_css_js %} - - {% endblock %} diff --git a/apps/ops/templates/ops/task_adhoc.html b/apps/ops/templates/ops/task_adhoc.html index 866ef4255..49035c725 100644 --- a/apps/ops/templates/ops/task_adhoc.html +++ b/apps/ops/templates/ops/task_adhoc.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} {% block content %} @@ -105,7 +103,7 @@ $(document).ready(function () { if (!cellData) { $(td).html("") } else { - $(td).html(cellData.user) + $(td).html(cellData) } }}, {targets: 6, createdCell: function (td, cellData) { @@ -120,8 +118,12 @@ $(document).ready(function () { }} ], ajax_url: '{% url "api-ops:adhoc-list" %}?task={{ object.pk }}', - columns: [{data: function(){return ""}}, {data: "short_id" }, {data: "hosts", orderable:false}, {data: "pattern", orderable:false}, - {data: "run_as"}, {data: "become", orderable:false}, {data: "date_created"}, {data: "id", orderable:false}] + columns: [ + {data: function(){return ""}}, {data: "short_id"}, + {data: "hosts", orderable:false}, {data: "pattern", orderable:false}, + {data: "run_as"}, {data: "become_display", orderable:false}, + {data: "date_created"}, {data: "id", orderable:false} + ] }; jumpserver.initDataTable(options); }).on('click', '.celery-task-log', function () { diff --git a/apps/ops/templates/ops/task_detail.html b/apps/ops/templates/ops/task_detail.html index c29fc2c4e..d2e46cc35 100644 --- a/apps/ops/templates/ops/task_detail.html +++ b/apps/ops/templates/ops/task_detail.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} @@ -82,11 +80,23 @@ {% trans 'Is finished' %}: - {{ object.latest_history.is_finished|yesno:"Yes,No,Unkown" }} + + {% if object.latest_history.is_finished %} + {% trans 'Yes' %} + {% else %} + {% trans 'No' %} + {% endif %} + {% trans 'Is success ' %}: - {{ object.latest_history.is_success|yesno:"Yes,No,Unkown" }} + + {% if object.latest_history.is_success %} + {% trans 'Yes' %} + {% else %} + {% trans 'No' %} + {% endif %} + {% trans 'Contents' %}: diff --git a/apps/ops/templates/ops/task_history.html b/apps/ops/templates/ops/task_history.html index dea4c9324..5121a8045 100644 --- a/apps/ops/templates/ops/task_history.html +++ b/apps/ops/templates/ops/task_history.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} {% block content %} diff --git a/apps/ops/templates/ops/task_list.html b/apps/ops/templates/ops/task_list.html index 83da62071..0d2095d78 100644 --- a/apps/ops/templates/ops/task_list.html +++ b/apps/ops/templates/ops/task_list.html @@ -10,7 +10,6 @@ {% trans 'Name' %} {% trans 'Run times' %} - {% trans 'Versions' %} {% trans 'Hosts' %} {% trans 'Success' %} {% trans 'Date' %} @@ -36,34 +35,40 @@ $(document).ready(function () { $(td).html(innerHtml); }}, {targets: 2, createdCell: function (td, cellData) { + var summary = cellData ? cellData.stat : {failed: 0, success: 0, total: 0}; var innerHtml = 'failed/success/total'; - if (cellData) { - innerHtml = innerHtml.replace('failed', cellData.failed) - .replace('success', cellData.success) - .replace('total', cellData.total); - $(td).html(innerHtml); - } else { - $(td).html('') - } + innerHtml = innerHtml.replace('failed', summary.failed) + .replace('success', summary.success) + .replace('total', summary.total); + $(td).html(innerHtml); }}, - {targets: 5, createdCell: function (td, cellData) { + {targets: 3, createdCell: function (td, cellData) { + var hostsAmount = cellData ? cellData.hosts_amount : 0; + $(td).html(hostsAmount) + }}, + {targets: 4, createdCell: function (td, cellData) { var successBtn = ''; var failedBtn = ''; - if (cellData) { + if (cellData && cellData.is_success) { $(td).html(successBtn) } else { $(td).html(failedBtn) } }}, - {targets: 6, createdCell: function (td, cellData) { - $(td).html(toSafeLocalDateStr(cellData)); + {targets: 5, createdCell: function (td, cellData) { + if (cellData) { + $(td).html(toSafeLocalDateStr(cellData.date_start)); + } else { + $(td).html(''); + } }}, - {targets: 7, createdCell: function (td, cellData) { + {targets: 6, createdCell: function (td, cellData) { + cellData = cellData ? cellData.timedelta : 0; var delta = readableSecond(cellData); $(td).html(delta); }}, { - targets: 8, + targets: 7, createdCell: function (td, cellData, rowData) { var runBtn = '{% trans "Run" %} '.replace('ID', cellData); var delBtn = '{% trans "Delete" %}'.replace('ID', cellData); @@ -73,10 +78,11 @@ $(document).ready(function () { ], ajax_url: '{% url "api-ops:task-list" %}', columns: [ - {data: "id"}, {data: "name", className: "text-left"}, {data: "history_summary", orderable: false}, - {data: "versions", orderable: false}, {data: "assets_amount", orderable: false}, - {data: "is_success", orderable: false}, {data: "date_updated"}, - {data: "timedelta", orderable:false}, {data: "id", orderable: false}, + {data: "id"}, {data: "name", className: "text-left"}, + {data: "latest_history", orderable: false}, + {data: "latest_history", orderable: false}, + {data: "latest_history", orderable: false}, {data: "latest_history"}, + {data: "latest_history", orderable:false}, {data: "id", orderable: false}, ], order: [], op_html: $('#actions').html() diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 5f955540d..cc242f649 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -13,6 +13,7 @@ router.register(r'tasks', api.TaskViewSet, 'task') router.register(r'adhoc', api.AdHocViewSet, 'adhoc') router.register(r'history', api.AdHocRunHistoryViewSet, 'history') router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution') +router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task') urlpatterns = [ path('tasks//run/', api.TaskRun.as_view(), name='task-run'), diff --git a/apps/ops/urls/view_urls.py b/apps/ops/urls/view_urls.py index f8428a667..13759c3f2 100644 --- a/apps/ops/urls/view_urls.py +++ b/apps/ops/urls/view_urls.py @@ -20,5 +20,5 @@ urlpatterns = [ path('celery/task//log/', views.CeleryTaskLogView.as_view(), name='celery-task-log'), path('command-execution/', views.CommandExecutionListView.as_view(), name='command-execution-list'), - path('command-execution/start/', views.CommandExecutionStartView.as_view(), name='command-execution-start'), + path('command-execution/create/', views.CommandExecutionCreateView.as_view(), name='command-execution-create'), ] diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 70cba27ba..3322890f2 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -1,6 +1,8 @@ # ~*~ coding: utf-8 ~*~ from django.utils.translation import ugettext_lazy as _ + from common.utils import get_logger, get_object_or_none +from common.tasks import send_mail_async from orgs.utils import set_to_root_org from .models import Task, AdHoc @@ -56,4 +58,14 @@ def update_or_create_ansible_task( return task, created +def send_server_performance_mail(path, usage, usages): + from users.models import User + subject = _("Disk used more than 80%: {} => {}").format(path, usage.percent) + message = subject + admins = User.objects.filter(role=User.ROLE_ADMIN) + recipient_list = [u.email for u in admins if u.email] + logger.info(subject) + send_mail_async(subject, message, recipient_list, html_message=message) + + diff --git a/apps/ops/views/celery.py b/apps/ops/views/celery.py index 1fcd18d08..9ae2d9755 100644 --- a/apps/ops/views/celery.py +++ b/apps/ops/views/celery.py @@ -17,6 +17,6 @@ class CeleryTaskLogView(PermissionsMixin, TemplateView): context = super().get_context_data(**kwargs) context.update({ 'task_id': self.kwargs.get('pk'), - 'ws_port': settings.CONFIG.WS_LISTEN_PORT + 'ws_port': settings.WS_LISTEN_PORT }) return context diff --git a/apps/ops/views/command.py b/apps/ops/views/command.py index 0824b4b93..87e0528c6 100644 --- a/apps/ops/views/command.py +++ b/apps/ops/views/command.py @@ -15,7 +15,7 @@ from ..forms import CommandExecutionForm __all__ = [ - 'CommandExecutionListView', 'CommandExecutionStartView' + 'CommandExecutionListView', 'CommandExecutionCreateView' ] @@ -55,7 +55,7 @@ class CommandExecutionListView(PermissionsMixin, DatetimeSearchMixin, ListView): return super().get_context_data(**kwargs) -class CommandExecutionStartView(PermissionsMixin, TemplateView): +class CommandExecutionCreateView(PermissionsMixin, TemplateView): template_name = 'ops/command_execution_create.html' form_class = CommandExecutionForm permission_classes = [IsValidUser] @@ -80,7 +80,7 @@ class CommandExecutionStartView(PermissionsMixin, TemplateView): 'action': _('Command execution'), 'form': self.get_form(), 'system_users': system_users, - 'ws_port': settings.CONFIG.WS_LISTEN_PORT + 'ws_port': settings.WS_LISTEN_PORT } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index 8fb7f30dd..64314a3f8 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -46,7 +46,11 @@ class OrgModelViewSet(CommonApiMixin, OrgQuerySetMixin, ModelViewSet): class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet): def allow_bulk_destroy(self, qs, filtered): - if qs.count() <= filtered.count(): + qs_count = qs.count() + filtered_count = filtered.count() + if filtered_count == 1: + return True + if qs_count <= filtered_count: return False if self.request.query_params.get('spm', ''): return True diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 544e50712..a9040b378 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -95,6 +95,14 @@ class Organization(models.Model): def get_org_admins(self): return self.org_admins + def org_id(self): + if self.is_real(): + return self.id + elif self.is_root(): + return None + else: + return '' + @lazyproperty def org_auditors(self): from users.models import User diff --git a/apps/perms/api/__init__.py b/apps/perms/api/__init__.py index 00dd28776..61cbd7d58 100644 --- a/apps/perms/api/__init__.py +++ b/apps/perms/api/__init__.py @@ -3,6 +3,10 @@ from .asset_permission import * from .user_permission import * +from .asset_permission_relation import * from .user_group_permission import * from .remote_app_permission import * from .user_remote_app_permission import * +from .database_app_permission import * +from .database_app_permission_relation import * +from .user_database_app_permission import * diff --git a/apps/perms/api/asset_permission.py b/apps/perms/api/asset_permission.py index 0243f8f1b..724b1d197 100644 --- a/apps/perms/api/asset_permission.py +++ b/apps/perms/api/asset_permission.py @@ -2,12 +2,9 @@ # from django.db.models import Q -from rest_framework.views import Response -from django.shortcuts import get_object_or_404 from common.permissions import IsOrgAdmin from orgs.mixins.api import OrgModelViewSet -from orgs.mixins import generics from common.utils import get_object_or_none from ..models import AssetPermission from ..hands import ( @@ -17,9 +14,7 @@ from .. import serializers __all__ = [ - 'AssetPermissionViewSet', 'AssetPermissionRemoveUserApi', - 'AssetPermissionAddUserApi', 'AssetPermissionRemoveAssetApi', - 'AssetPermissionAddAssetApi', 'AssetPermissionAssetsApi', + 'AssetPermissionViewSet', ] @@ -28,7 +23,10 @@ class AssetPermissionViewSet(OrgModelViewSet): 资产授权列表的增删改查api """ model = AssetPermission - serializer_class = serializers.AssetPermissionCreateUpdateSerializer + serializer_classes = { + 'default': serializers.AssetPermissionCreateUpdateSerializer, + 'display': serializers.AssetPermissionListSerializer + } filter_fields = ['name'] permission_classes = (IsOrgAdmin,) @@ -38,11 +36,9 @@ class AssetPermissionViewSet(OrgModelViewSet): ) return queryset - def get_serializer_class(self): - if self.action in ("list", 'retrieve') and \ - self.request.query_params.get("display"): - return serializers.AssetPermissionListSerializer - return self.serializer_class + def is_query_all(self): + query_all = self.request.query_params.get('all', '1') == '1' + return query_all def filter_valid(self, queryset): valid_query = self.request.query_params.get('is_valid', None) @@ -81,7 +77,10 @@ class AssetPermissionViewSet(OrgModelViewSet): if not _nodes: return queryset.none() - nodes = set() + if not self.is_query_all(): + queryset = queryset.filter(nodes__in=_nodes) + return queryset + nodes = set(_nodes) for node in _nodes: nodes |= set(node.get_ancestors(with_self=True)) queryset = queryset.filter(nodes__in=nodes) @@ -101,6 +100,9 @@ class AssetPermissionViewSet(OrgModelViewSet): return queryset if not assets: return queryset.none() + if not self.is_query_all(): + queryset = queryset.filter(assets__in=assets) + return queryset inherit_all_nodes = set() inherit_nodes_keys = assets.all().values_list('nodes__key', flat=True) @@ -117,7 +119,6 @@ class AssetPermissionViewSet(OrgModelViewSet): def filter_user(self, queryset): user_id = self.request.query_params.get('user_id') username = self.request.query_params.get('username') - query_group = self.request.query_params.get('all') if user_id: user = get_object_or_none(User, pk=user_id) elif username: @@ -126,14 +127,14 @@ class AssetPermissionViewSet(OrgModelViewSet): return queryset if not user: return queryset.none() - kwargs = {} - args = [] - if query_group: - groups = user.groups.all() - args.append(Q(users=user) | Q(user_groups__in=groups)) - else: - kwargs["users"] = user - return queryset.filter(*args, **kwargs).distinct() + if not self.is_query_all(): + queryset = queryset.filter(users=user) + return queryset + groups = user.groups.all() + queryset = queryset.filter( + Q(users=user) | Q(user_groups__in=groups) + ).distinct() + return queryset def filter_user_group(self, queryset): user_group_id = self.request.query_params.get('user_group_id') @@ -167,99 +168,3 @@ class AssetPermissionViewSet(OrgModelViewSet): queryset = self.filter_user_group(queryset) queryset = queryset.distinct() return queryset - - -class AssetPermissionRemoveUserApi(generics.RetrieveUpdateAPIView): - """ - 将用户从授权中移除,Detail页面会调用 - """ - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateUserSerializer - - 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)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionAddUserApi(generics.RetrieveUpdateAPIView): - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateUserSerializer - - 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)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionRemoveAssetApi(generics.RetrieveUpdateAPIView): - """ - 将用户从授权中移除,Detail页面会调用 - """ - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateAssetSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data.get('assets') - if assets: - perm.assets.remove(*tuple(assets)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionAddAssetApi(generics.RetrieveUpdateAPIView): - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateAssetSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data.get('assets') - if assets: - perm.assets.add(*tuple(assets)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionAssetsApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionAssetsSerializer - filter_fields = ("hostname", "ip") - search_fields = filter_fields - - def get_object(self): - pk = self.kwargs.get('pk') - return get_object_or_404(AssetPermission, pk=pk) - - def get_queryset(self): - perm = self.get_object() - assets = perm.get_all_assets().only( - *self.serializer_class.Meta.only_fields - ) - return assets diff --git a/apps/perms/api/asset_permission_relation.py b/apps/perms/api/asset_permission_relation.py new file mode 100644 index 000000000..3b68af775 --- /dev/null +++ b/apps/perms/api/asset_permission_relation.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import generics +from django.db.models import F, Value +from django.db.models.functions import Concat +from django.shortcuts import get_object_or_404 + +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.utils import current_org +from common.permissions import IsOrgAdmin +from .. import serializers +from .. import models + +__all__ = [ + 'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet', + 'AssetPermissionAssetRelationViewSet', 'AssetPermissionNodeRelationViewSet', + 'AssetPermissionSystemUserRelationViewSet', 'AssetPermissionAllAssetListApi', + 'AssetPermissionAllUserListApi', +] + + +class RelationMixin(OrgBulkModelViewSet): + def get_queryset(self): + queryset = self.model.objects.all() + org_id = current_org.org_id() + if org_id is not None: + queryset = queryset.filter(assetpermission__org_id=org_id) + queryset = queryset.annotate(assetpermission_display=F('assetpermission__name')) + return queryset + + +class AssetPermissionUserRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionUserRelationSerializer + model = models.AssetPermission.users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', "user", "assetpermission", + ] + search_fields = ("user__name", "user__username", "assetpermission__name") + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(user_display=F('user__name')) + return queryset + + +class AssetPermissionAllUserListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.AssetPermissionAllUserSerializer + filter_fields = ("username", "name") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.AssetPermission, pk=pk) + users = perm.get_all_users().only( + *self.serializer_class.Meta.only_fields + ) + return users + + +class AssetPermissionUserGroupRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionUserGroupRelationSerializer + model = models.AssetPermission.user_groups.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', "usergroup", "assetpermission" + ] + search_fields = ["usergroup__name", "assetpermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(usergroup_display=F('usergroup__name')) + return queryset + + +class AssetPermissionAssetRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionAssetRelationSerializer + model = models.AssetPermission.assets.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'asset', 'assetpermission', + ] + search_fields = ["id", "asset__hostname", "asset__ip", "assetpermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(asset_display=F('asset__hostname')) + return queryset + + +class AssetPermissionAllAssetListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.AssetPermissionAllAssetSerializer + filter_fields = ("hostname", "ip") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.AssetPermission, pk=pk) + assets = perm.get_all_assets().only( + *self.serializer_class.Meta.only_fields + ) + return assets + + +class AssetPermissionNodeRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionNodeRelationSerializer + model = models.AssetPermission.nodes.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'node', 'assetpermission', + ] + search_fields = ["node__value", "assetpermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(node_key=F('node__key')) + return queryset + + +class AssetPermissionSystemUserRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionSystemUserRelationSerializer + model = models.AssetPermission.system_users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'systemuser', 'assetpermission', + ] + search_fields = [ + "assetpermission__name", "systemuser__name", "systemuser__username" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(systemuser_display=Concat( + F('systemuser__name'), Value('('), F('systemuser__username'), + Value(')') + )) + return queryset diff --git a/apps/perms/api/database_app_permission.py b/apps/perms/api/database_app_permission.py new file mode 100644 index 000000000..4b43d347f --- /dev/null +++ b/apps/perms/api/database_app_permission.py @@ -0,0 +1,21 @@ +# coding: utf-8 +# + +from orgs.mixins.api import OrgBulkModelViewSet + +from .. import models, serializers +from common.permissions import IsOrgAdmin + + +__all__ = ['DatabaseAppPermissionViewSet'] + + +class DatabaseAppPermissionViewSet(OrgBulkModelViewSet): + model = models.DatabaseAppPermission + serializer_classes = { + 'default': serializers.DatabaseAppPermissionSerializer, + 'display': serializers.DatabaseAppPermissionListSerializer + } + filter_fields = ('name',) + search_fields = filter_fields + permission_classes = (IsOrgAdmin,) diff --git a/apps/perms/api/database_app_permission_relation.py b/apps/perms/api/database_app_permission_relation.py new file mode 100644 index 000000000..32ab34355 --- /dev/null +++ b/apps/perms/api/database_app_permission_relation.py @@ -0,0 +1,132 @@ +# coding: utf-8 +# + +from rest_framework import generics +from django.db.models import F, Value +from django.db.models.functions import Concat +from django.shortcuts import get_object_or_404 + +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.utils import current_org +from common.permissions import IsOrgAdmin +from .. import models, serializers + +__all__ = [ + 'DatabaseAppPermissionUserRelationViewSet', + 'DatabaseAppPermissionUserGroupRelationViewSet', + 'DatabaseAppPermissionAllUserListApi', + 'DatabaseAppPermissionDatabaseAppRelationViewSet', + 'DatabaseAppPermissionAllDatabaseAppListApi', + 'DatabaseAppPermissionSystemUserRelationViewSet', +] + + +class RelationMixin(OrgBulkModelViewSet): + def get_queryset(self): + queryset = self.model.objects.all() + org_id = current_org.org_id() + if org_id is not None: + queryset = queryset.filter(databaseapppermission__org_id=org_id) + queryset = queryset.annotate(databaseapppermission_display=F('databaseapppermission__name')) + return queryset + + +class DatabaseAppPermissionUserRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionUserRelationSerializer + model = models.DatabaseAppPermission.users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'user', 'databaseapppermission' + ] + search_fields = ('user__name', 'user__username', 'databaseapppermission__name') + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(user_display=F('user__name')) + return queryset + + +class DatabaseAppPermissionUserGroupRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionUserGroupRelationSerializer + model = models.DatabaseAppPermission.user_groups.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', "usergroup", "databaseapppermission" + ] + search_fields = ["usergroup__name", "databaseapppermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(usergroup_display=F('usergroup__name')) + return queryset + + +class DatabaseAppPermissionAllUserListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.DatabaseAppPermissionAllUserSerializer + filter_fields = ("username", "name") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.DatabaseAppPermission, pk=pk) + users = perm.get_all_users().only( + *self.serializer_class.Meta.only_fields + ) + return users + + +class DatabaseAppPermissionDatabaseAppRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionDatabaseAppRelationSerializer + model = models.DatabaseAppPermission.database_apps.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'databaseapp', 'databaseapppermission', + ] + search_fields = [ + "id", "databaseapp__name", "databaseapppermission__name" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(databaseapp_display=F('databaseapp__name')) + return queryset + + +class DatabaseAppPermissionAllDatabaseAppListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.DatabaseAppPermissionAllDatabaseAppSerializer + filter_fields = ("name",) + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.DatabaseAppPermission, pk=pk) + database_apps = perm.get_all_database_apps().only( + *self.serializer_class.Meta.only_fields + ) + return database_apps + + +class DatabaseAppPermissionSystemUserRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionSystemUserRelationSerializer + model = models.DatabaseAppPermission.system_users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'systemuser', 'databaseapppermission' + ] + search_fields = [ + 'databaseapppermission__name', 'systemuser__name', 'systemuser__username' + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate( + systemuser_display=Concat( + F('systemuser__name'), Value('('), F('systemuser__username'), + Value(')') + ) + ) + return queryset diff --git a/apps/perms/api/remote_app_permission.py b/apps/perms/api/remote_app_permission.py index 6ced7f0ae..b7fa6de19 100644 --- a/apps/perms/api/remote_app_permission.py +++ b/apps/perms/api/remote_app_permission.py @@ -11,6 +11,7 @@ from ..serializers import ( RemoteAppPermissionSerializer, RemoteAppPermissionUpdateUserSerializer, RemoteAppPermissionUpdateRemoteAppSerializer, + RemoteAppPermissionListSerializer, ) @@ -25,7 +26,10 @@ class RemoteAppPermissionViewSet(OrgModelViewSet): model = RemoteAppPermission filter_fields = ('name', ) search_fields = filter_fields - serializer_class = RemoteAppPermissionSerializer + serializer_classes = { + 'default': RemoteAppPermissionSerializer, + 'display': RemoteAppPermissionListSerializer, + } permission_classes = (IsOrgAdmin,) diff --git a/apps/perms/api/user_database_app_permission.py b/apps/perms/api/user_database_app_permission.py new file mode 100644 index 000000000..3a973b8c1 --- /dev/null +++ b/apps/perms/api/user_database_app_permission.py @@ -0,0 +1,127 @@ +# coding: utf-8 +# + +import uuid +from django.shortcuts import get_object_or_404 +from rest_framework.views import APIView, Response +from common.permissions import IsOrgAdminOrAppUser, IsValidUser +from common.tree import TreeNodeSerializer +from orgs.mixins import generics +from users.models import User, UserGroup +from applications.serializers import DatabaseAppSerializer +from applications.models import DatabaseApp +from assets.models import SystemUser +from .. import utils, serializers +from .mixin import UserPermissionMixin + +__all__ = [ + 'UserGrantedDatabaseAppsApi', + 'UserGrantedDatabaseAppsAsTreeApi', + 'UserGroupGrantedDatabaseAppsApi', + 'ValidateUserDatabaseAppPermissionApi', + 'UserGrantedDatabaseAppSystemUsersApi', +] + + +class UserGrantedDatabaseAppsApi(generics.ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = DatabaseAppSerializer + filter_fields = ['id', 'name'] + search_fields = ['name'] + + 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 = utils.DatabaseAppPermissionUtil(self.get_object()) + queryset = util.get_database_apps() + return queryset + + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() + + +class UserGrantedDatabaseAppsAsTreeApi(UserGrantedDatabaseAppsApi): + serializer_class = TreeNodeSerializer + permission_classes = (IsOrgAdminOrAppUser,) + + def get_serializer(self, database_apps, *args, **kwargs): + if database_apps is None: + database_apps = [] + only_database_app = self.request.query_params.get('only', '0') == '1' + tree_root = None + data = [] + if not only_database_app: + tree_root = utils.construct_database_apps_tree_root() + data.append(tree_root) + for database_app in database_apps: + node = utils.parse_database_app_to_tree_node(tree_root, database_app) + data.append(node) + data.sort() + return super().get_serializer(data, many=True) + + +class UserGrantedDatabaseAppSystemUsersApi(UserPermissionMixin, generics.ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.DatabaseAppSystemUserSerializer + only_fields = serializers.DatabaseAppSystemUserSerializer.Meta.only_fields + + def get_queryset(self): + util = utils.DatabaseAppPermissionUtil(self.obj) + database_app_id = self.kwargs.get('database_app_id') + database_app = get_object_or_404(DatabaseApp, id=database_app_id) + system_users = util.get_database_app_system_users(database_app) + return system_users + + +# Validate + +class ValidateUserDatabaseAppPermissionApi(APIView): + permission_classes = (IsOrgAdminOrAppUser,) + + def get(self, request, *args, **kwargs): + user_id = request.query_params.get('user_id', '') + database_app_id = request.query_params.get('database_app_id', '') + system_user_id = request.query_params.get('system_user_id', '') + + try: + user_id = uuid.UUID(user_id) + database_app_id = uuid.UUID(database_app_id) + system_user_id = uuid.UUID(system_user_id) + except ValueError: + return Response({'msg': False}, status=403) + + user = get_object_or_404(User, id=user_id) + database_app = get_object_or_404(DatabaseApp, id=database_app_id) + system_user = get_object_or_404(SystemUser, id=system_user_id) + + util = utils.DatabaseAppPermissionUtil(user) + system_users = util.get_database_app_system_users(database_app) + if system_user in system_users: + return Response({'msg': True}, status=200) + + return Response({'msg': False}, status=403) + + +# UserGroup + +class UserGroupGrantedDatabaseAppsApi(generics.ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = DatabaseAppSerializer + + 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 = utils.DatabaseAppPermissionUtil(user_group) + queryset = util.get_database_apps() + return queryset diff --git a/apps/perms/api/user_permission/common.py b/apps/perms/api/user_permission/common.py index 7e4271af8..003e38856 100644 --- a/apps/perms/api/user_permission/common.py +++ b/apps/perms/api/user_permission/common.py @@ -59,6 +59,9 @@ class GetUserAssetPermissionActionsApi(UserAssetPermissionMixin, class ValidateUserAssetPermissionApi(UserAssetPermissionMixin, APIView): permission_classes = (IsOrgAdminOrAppUser,) + def get_cache_policy(self): + return 0 + def get_obj(self): user_id = self.request.query_params.get('user_id', '') user = get_object_or_404(User, id=user_id) diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index 4a004e0d1..bbc926ffe 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -10,9 +10,12 @@ from ...hands import Node, Asset class UserAssetPermissionMixin(UserPermissionMixin): util = None + def get_cache_policy(self): + return self.request.query_params.get('cache_policy', '0') + @lazyproperty def util(self): - cache_policy = self.request.query_params.get('cache_policy', '0') + cache_policy = self.get_cache_policy() system_user_id = self.request.query_params.get("system_user") util = AssetPermissionUtilV2(self.obj, cache_policy=cache_policy) if system_user_id: diff --git a/apps/perms/forms/__init__.py b/apps/perms/forms/__init__.py index 129901afc..c6581b858 100644 --- a/apps/perms/forms/__init__.py +++ b/apps/perms/forms/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/apps/perms/forms/asset_permission.py b/apps/perms/forms/asset_permission.py index 8282f0880..92ca8030f 100644 --- a/apps/perms/forms/asset_permission.py +++ b/apps/perms/forms/asset_permission.py @@ -5,8 +5,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from orgs.mixins.forms import OrgModelForm -from orgs.utils import current_org -from assets.models import Asset, Node +from assets.models import Asset, Node, SystemUser from ..models import AssetPermission, Action __all__ = [ @@ -58,6 +57,12 @@ class AssetPermissionForm(OrgModelForm): nodes_field.queryset = Node.objects.none() users_field.queryset = [] + # 过滤系统用户 + system_users_field = self.fields.get('system_users') + system_users_field.queryset = SystemUser.objects.exclude( + protocol=SystemUser.PROTOCOL_MYSQL + ) + def set_nodes_initial(self, nodes): field = self.fields['nodes'] field.choices = [(n.id, n.full_value) for n in nodes] diff --git a/apps/perms/forms/database_app_permission.py b/apps/perms/forms/database_app_permission.py new file mode 100644 index 000000000..57ce4d0e3 --- /dev/null +++ b/apps/perms/forms/database_app_permission.py @@ -0,0 +1,49 @@ +# coding: utf-8 +# + +from django.utils.translation import ugettext as _ +from django import forms +from orgs.mixins.forms import OrgModelForm +from assets.models import SystemUser + +from ..models import DatabaseAppPermission + + +__all__ = ['DatabaseAppPermissionCreateUpdateForm'] + + +class DatabaseAppPermissionCreateUpdateForm(OrgModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + users_field = self.fields.get('users') + if self.instance: + users_field.queryset = self.instance.users.all() + else: + users_field.queryset = [] + + # 过滤系统用户 + system_users_field = self.fields.get('system_users') + system_users_field.queryset = SystemUser.objects.filter( + protocol=SystemUser.PROTOCOL_MYSQL + ) + + class Meta: + model = DatabaseAppPermission + exclude = ( + 'id', 'date_created', 'created_by', 'org_id' + ) + widgets = { + 'users': forms.SelectMultiple( + attrs={'class': 'users-select2', 'data-placeholder': _('User')} + ), + 'user_groups': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('User group')} + ), + 'database_apps': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('DatabaseApp')} + ), + 'system_users': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('System users')} + ), + } diff --git a/apps/perms/forms/remote_app_permission.py b/apps/perms/forms/remote_app_permission.py index abac92e05..c4179297f 100644 --- a/apps/perms/forms/remote_app_permission.py +++ b/apps/perms/forms/remote_app_permission.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from django import forms from orgs.mixins.forms import OrgModelForm -from orgs.utils import current_org +from assets.models import SystemUser from ..models import RemoteAppPermission @@ -24,6 +24,12 @@ class RemoteAppPermissionCreateUpdateForm(OrgModelForm): else: users_field.queryset = [] + # 过滤系统用户 + system_users_field = self.fields.get('system_users') + system_users_field.queryset = SystemUser.objects.filter( + protocol=SystemUser.PROTOCOL_RDP + ) + class Meta: model = RemoteAppPermission exclude = ( @@ -43,13 +49,3 @@ class RemoteAppPermissionCreateUpdateForm(OrgModelForm): attrs={'class': 'select2', 'data-placeholder': _('System user')} ) } - - 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'] diff --git a/apps/perms/migrations/0010_auto_20191218_1705.py b/apps/perms/migrations/0010_auto_20191218_1705.py new file mode 100644 index 000000000..c3144bc5d --- /dev/null +++ b/apps/perms/migrations/0010_auto_20191218_1705.py @@ -0,0 +1,47 @@ +# Generated by Django 2.1.11 on 2019-12-18 09:05 + +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', '0024_auto_20191118_1612'), + ('assets', '0046_auto_20191218_1705'), + ('applications', '0004_auto_20191218_1705'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('perms', '0009_remoteapppermission_system_users'), + ] + + operations = [ + migrations.CreateModel( + name='DatabaseAppPermission', + 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')), + ('database_apps', models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='applications.DatabaseApp', verbose_name='DatabaseApp')), + ('system_users', models.ManyToManyField(related_name='granted_by_database_app_permissions', to='assets.SystemUser', verbose_name='System user')), + ('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': 'DatabaseApp permission', + 'ordering': ('name',), + }, + ), + migrations.AlterUniqueTogether( + name='databaseapppermission', + unique_together={('org_id', 'name')}, + ), + ] diff --git a/apps/perms/models/__init__.py b/apps/perms/models/__init__.py index 129901afc..c6581b858 100644 --- a/apps/perms/models/__init__.py +++ b/apps/perms/models/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index 878d9d739..950d54b10 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -80,9 +80,10 @@ class BasePermission(OrgModelMixin): 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)) + from users.models import User + users_id = self.users.all().values_list('id', flat=True) + groups_id = self.user_groups.all().values_list('id', flat=True) + users = User.objects.filter( + Q(id__in=users_id) | Q(groups__id__in=groups_id) + ).distinct() return users diff --git a/apps/perms/models/database_app_permission.py b/apps/perms/models/database_app_permission.py new file mode 100644 index 000000000..de2693274 --- /dev/null +++ b/apps/perms/models/database_app_permission.py @@ -0,0 +1,30 @@ +# coding: utf-8 +# + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .base import BasePermission + +__all__ = [ + 'DatabaseAppPermission', +] + + +class DatabaseAppPermission(BasePermission): + database_apps = models.ManyToManyField( + 'applications.DatabaseApp', related_name='granted_by_permissions', + blank=True, verbose_name=_("DatabaseApp") + ) + system_users = models.ManyToManyField( + 'assets.SystemUser', related_name='granted_by_database_app_permissions', + verbose_name=_("System user") + ) + + class Meta: + unique_together = [('org_id', 'name')] + verbose_name = _('DatabaseApp permission') + ordering = ('name',) + + def get_all_database_apps(self): + return self.database_apps.all() diff --git a/apps/perms/serializers/__init__.py b/apps/perms/serializers/__init__.py index 1d099cb33..7f83bae9b 100644 --- a/apps/perms/serializers/__init__.py +++ b/apps/perms/serializers/__init__.py @@ -4,3 +4,6 @@ from .asset_permission import * from .user_permission import * from .remote_app_permission import * +from .asset_permission_relation import * +from .database_app_permission import * +from .database_app_permission_relation import * diff --git a/apps/perms/serializers/asset_permission.py b/apps/perms/serializers/asset_permission.py index 94a7dfdbd..73612a7e6 100644 --- a/apps/perms/serializers/asset_permission.py +++ b/apps/perms/serializers/asset_permission.py @@ -6,12 +6,10 @@ from rest_framework import serializers from common.fields import StringManyToManyField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action -from assets.models import Asset __all__ = [ 'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer', - 'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer', - 'ActionsField', 'AssetPermissionAssetsSerializer', + 'ActionsField', ] @@ -59,23 +57,4 @@ class AssetPermissionListSerializer(BulkOrgResourceModelSerializer): fields = '__all__' -class AssetPermissionUpdateUserSerializer(serializers.ModelSerializer): - class Meta: - model = AssetPermission - fields = ['id', 'users'] - - -class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer): - - class Meta: - model = AssetPermission - fields = ['id', 'assets'] - - -class AssetPermissionAssetsSerializer(serializers.ModelSerializer): - - class Meta: - model = Asset - only_fields = ['id', 'hostname', 'ip'] - fields = tuple(only_fields) diff --git a/apps/perms/serializers/asset_permission_relation.py b/apps/perms/serializers/asset_permission_relation.py new file mode 100644 index 000000000..808f40468 --- /dev/null +++ b/apps/perms/serializers/asset_permission_relation.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers + +from common.mixins import BulkSerializerMixin +from common.serializers import AdaptedBulkListSerializer +from assets.models import Asset, Node +from ..models import AssetPermission +from users.models import User + +__all__ = [ + 'AssetPermissionUserRelationSerializer', + 'AssetPermissionUserGroupRelationSerializer', + "AssetPermissionAssetRelationSerializer", + 'AssetPermissionNodeRelationSerializer', + 'AssetPermissionSystemUserRelationSerializer', + 'AssetPermissionAllAssetSerializer', + 'AssetPermissionAllUserSerializer', +] + + +class CurrentAssetPermission(object): + permission = None + + def set_context(self, serializer_field): + self.permission = serializer_field.context['permission'] + + def __call__(self): + return self.permission + + +class RelationMixin(BulkSerializerMixin, serializers.Serializer): + assetpermission_display = serializers.ReadOnlyField() + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(['assetpermission', "assetpermission_display"]) + return fields + + class Meta: + list_serializer_class = AdaptedBulkListSerializer + + +class AssetPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + user_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.users.through + fields = [ + 'id', 'user', 'user_display', + ] + + +class AssetPermissionAllUserSerializer(serializers.Serializer): + user = serializers.UUIDField(read_only=True, source='id') + user_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'username', 'name'] + + @staticmethod + def get_user_display(obj): + return str(obj) + + +class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): + usergroup_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.user_groups.through + fields = [ + 'id', 'usergroup', "usergroup_display", + ] + + +class AssetPermissionAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): + asset_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.assets.through + fields = [ + 'id', "asset", "asset_display", + ] + + +class AssetPermissionAllAssetSerializer(serializers.Serializer): + asset = serializers.UUIDField(read_only=True, source='id') + asset_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'hostname', 'ip'] + + @staticmethod + def get_asset_display(obj): + return str(obj) + + +class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): + node_display = serializers.SerializerMethodField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.nodes.through + fields = [ + 'id', 'node', "node_display", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tree = Node.tree() + + def get_node_display(self, obj): + if hasattr(obj, 'node_key'): + return self.tree.get_node_full_tag(obj.node_key) + else: + return obj.node.full_value + + +class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + systemuser_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.system_users.through + fields = [ + 'id', 'systemuser', 'systemuser_display' + ] diff --git a/apps/perms/serializers/database_app_permission.py b/apps/perms/serializers/database_app_permission.py new file mode 100644 index 000000000..a8b8bafcd --- /dev/null +++ b/apps/perms/serializers/database_app_permission.py @@ -0,0 +1,39 @@ +# coding: utf-8 +# + +from rest_framework import serializers + +from common.fields import StringManyToManyField +from common.serializers import AdaptedBulkListSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from .. import models + +__all__ = [ + 'DatabaseAppPermissionSerializer', 'DatabaseAppPermissionListSerializer' +] + + +class DatabaseAppPermissionSerializer(BulkOrgResourceModelSerializer): + class Meta: + model = models.DatabaseAppPermission + list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'name', 'users', 'user_groups', + 'database_apps', 'system_users', 'comment', 'is_active', + 'date_start', 'date_expired', 'is_valid', + 'created_by', 'date_created' + ] + read_only_fields = ['created_by', 'date_created'] + + +class DatabaseAppPermissionListSerializer(BulkOrgResourceModelSerializer): + users = StringManyToManyField(many=True, read_only=True) + user_groups = StringManyToManyField(many=True, read_only=True) + database_apps = StringManyToManyField(many=True, read_only=True) + system_users = StringManyToManyField(many=True, read_only=True) + is_valid = serializers.BooleanField() + is_expired = serializers.BooleanField() + + class Meta: + model = models.DatabaseAppPermission + fields = '__all__' diff --git a/apps/perms/serializers/database_app_permission_relation.py b/apps/perms/serializers/database_app_permission_relation.py new file mode 100644 index 000000000..1a8263cda --- /dev/null +++ b/apps/perms/serializers/database_app_permission_relation.py @@ -0,0 +1,94 @@ +# coding: utf-8 +# +from rest_framework import serializers + +from applications.models import DatabaseApp +from common.mixins import BulkSerializerMixin +from common.serializers import AdaptedBulkListSerializer + +from .. import models + +__all__ = [ + 'DatabaseAppPermissionUserRelationSerializer', + 'DatabaseAppPermissionUserGroupRelationSerializer', + 'DatabaseAppPermissionAllUserSerializer', + 'DatabaseAppPermissionDatabaseAppRelationSerializer', + 'DatabaseAppPermissionAllDatabaseAppSerializer', + 'DatabaseAppPermissionSystemUserRelationSerializer', +] + + +class RelationMixin(BulkSerializerMixin, serializers.Serializer): + databaseapppermission_display = serializers.ReadOnlyField() + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(['databaseapppermission', "databaseapppermission_display"]) + return fields + + class Meta: + list_serializer_class = AdaptedBulkListSerializer + + +class DatabaseAppPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + user_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.users.through + fields = [ + 'id', 'user', 'user_display', + ] + + +class DatabaseAppPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): + usergroup_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.user_groups.through + fields = [ + 'id', 'usergroup', "usergroup_display", + ] + + +class DatabaseAppPermissionAllUserSerializer(serializers.Serializer): + user = serializers.UUIDField(read_only=True, source='id') + user_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'username', 'name'] + + @staticmethod + def get_user_display(obj): + return str(obj) + + +class DatabaseAppPermissionDatabaseAppRelationSerializer(RelationMixin, serializers.ModelSerializer): + databaseapp_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.database_apps.through + fields = [ + 'id', "databaseapp", "databaseapp_display", + ] + + +class DatabaseAppPermissionAllDatabaseAppSerializer(serializers.Serializer): + databaseapp = serializers.UUIDField(read_only=True, source='id') + databaseapp_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'name'] + + @staticmethod + def get_databaseapp_display(obj): + return str(obj) + + +class DatabaseAppPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + systemuser_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.system_users.through + fields = [ + 'id', 'systemuser', 'systemuser_display' + ] diff --git a/apps/perms/serializers/remote_app_permission.py b/apps/perms/serializers/remote_app_permission.py index 4361cff88..41c5d7022 100644 --- a/apps/perms/serializers/remote_app_permission.py +++ b/apps/perms/serializers/remote_app_permission.py @@ -3,6 +3,7 @@ from rest_framework import serializers +from common.fields import StringManyToManyField from common.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import RemoteAppPermission @@ -12,6 +13,7 @@ __all__ = [ 'RemoteAppPermissionSerializer', 'RemoteAppPermissionUpdateUserSerializer', 'RemoteAppPermissionUpdateRemoteAppSerializer', + 'RemoteAppPermissionListSerializer', ] @@ -27,6 +29,19 @@ class RemoteAppPermissionSerializer(BulkOrgResourceModelSerializer): read_only_fields = ['created_by', 'date_created'] +class RemoteAppPermissionListSerializer(BulkOrgResourceModelSerializer): + users = StringManyToManyField(many=True, read_only=True) + user_groups = StringManyToManyField(many=True, read_only=True) + remote_apps = StringManyToManyField(many=True, read_only=True) + system_users = StringManyToManyField(many=True, read_only=True) + is_valid = serializers.BooleanField() + is_expired = serializers.BooleanField() + + class Meta: + model = RemoteAppPermission + fields = '__all__' + + class RemoteAppPermissionUpdateUserSerializer(serializers.ModelSerializer): class Meta: model = RemoteAppPermission diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index d83c15b6f..c194cf64a 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -13,6 +13,7 @@ __all__ = [ 'AssetGrantedSerializer', 'ActionsSerializer', 'AssetSystemUserSerializer', 'RemoteAppSystemUserSerializer', + 'DatabaseAppSystemUserSerializer', ] @@ -41,11 +42,22 @@ class RemoteAppSystemUserSerializer(serializers.ModelSerializer): read_only_fields = fields +class DatabaseAppSystemUserSerializer(serializers.ModelSerializer): + class Meta: + model = SystemUser + only_fields = ( + 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', + ) + fields = list(only_fields) + read_only_fields = fields + + class AssetGrantedSerializer(serializers.ModelSerializer): """ 被授权资产的数据结构 """ protocols = ProtocolsField(label=_('Protocols'), required=False, read_only=True) + platform = serializers.ReadOnlyField(source='platform_base') class Meta: model = Asset diff --git a/apps/perms/templates/perms/asset_permission_asset.html b/apps/perms/templates/perms/asset_permission_asset.html index f01f7cec8..b6079cdaa 100644 --- a/apps/perms/templates/perms/asset_permission_asset.html +++ b/apps/perms/templates/perms/asset_permission_asset.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -54,8 +50,8 @@ - {% trans 'Hostname' %} - {% trans 'IP' %} + {% trans 'Asset' %} + {% trans 'Action' %} @@ -75,7 +71,7 @@
    - @@ -112,10 +108,46 @@
    {% for node in asset_permission.nodes.all %} - + {{ node.full_value }} - + + + + {% endfor %} + + +
    +
    +
    +
    + {% trans 'System user' %} +
    +
    + + + + + + + + + + + + {% for system_user in object.system_users.all %} + + + {% endfor %} @@ -129,69 +161,83 @@ - {% include 'assets/_asset_list_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/perms/templates/perms/asset_permission_create_update.html b/apps/perms/templates/perms/asset_permission_create_update.html index 0c05de831..4cb5928eb 100644 --- a/apps/perms/templates/perms/asset_permission_create_update.html +++ b/apps/perms/templates/perms/asset_permission_create_update.html @@ -3,8 +3,6 @@ {% load static %} {% load bootstrap3 %} {% block custom_head_css_js %} - - {% endblock %} @@ -111,21 +109,14 @@ $(document).ready(function () { initDateRangePicker('#date_start'); initDateRangePicker('#date_expired'); - - $("#id_assets").parent().find(".select2-selection").on('click', function (e) { - if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){ - e.preventDefault(); - e.stopPropagation(); - $("#asset_list_modal").modal(); - } - }) + initAssetTreeModel('#id_assets'); }) .on("submit", "form", function (evt) { evt.preventDefault(); - var the_url = '{% url 'api-perms:asset-permission-list' %}'; + var theUrl = '{% url 'api-perms:asset-permission-list' %}'; var method = "POST"; {% if api_action == "update" %} - the_url = '{% url 'api-perms:asset-permission-detail' pk=object.id %}'; + theUrl = '{% url 'api-perms:asset-permission-detail' pk=object.id %}'; method = "PUT"; {% endif %} var redirect_to = '{% url "perms:asset-permission-list" %}'; @@ -135,7 +126,7 @@ $(document).ready(function () { objectAttrsIsDatetime(data, ['date_start', 'date_expired']); objectAttrsIsBool(data, ['is_active']); var props = { - url: the_url, + url: theUrl, data: data, method: method, form: form, diff --git a/apps/perms/templates/perms/asset_permission_detail.html b/apps/perms/templates/perms/asset_permission_detail.html index 044438c43..6930cfc5a 100644 --- a/apps/perms/templates/perms/asset_permission_detail.html +++ b/apps/perms/templates/perms/asset_permission_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -135,42 +130,7 @@
    -
    -
    - {% trans 'System user' %} -
    -
    -
    + +
    + +
    {{ system_user|truncatechars:21}} +
    - - - - - - - - - - {% for system_user in object.system_users.all %} - - - - - {% endfor %} - -
    - -
    - -
    {{ system_user }} - -
    -
    -
    @@ -182,17 +142,7 @@ @@ -14,56 +12,36 @@ .toggle { cursor: pointer; } - .detail-key { - width: 70px; - } {% endblock %} -{% block content %} -
    -
    -
    - {% include 'assets/_node_tree.html' %} -
    -
    -
    -
    - -
    -
    -
    -
    - - - -
    - - - - - - - - - - - - - - - - -
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'Asset' %}{% trans 'Node'%}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    -
    -
    -
    -
    - +{% block table_container %} +
    + + + +
    + + + + + + + + + + + + + + + + +
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'Asset' %}{% trans 'Node'%}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    {% include '_filter_dropdown.html' %} {% endblock %} @@ -96,9 +74,6 @@ function beforeNodeAsync(treeId, treeNode) { return true } -function makeLabel(data) { - return "" + data[1] + "
    " -} function format(d) { var data = ""; @@ -176,12 +151,12 @@ function initTable() { $(td).html(update_btn + del_btn); }} ], - ajax_url: '{% url "api-perms:asset-permission-list" %}?display=1', + ajax_url: '{% url "api-perms:asset-permission-list" %}', columns: [ {data: "id"}, {data: "name"}, {data: "users", orderable: false}, {data: "user_groups", orderable: false}, {data: "assets", orderable: false}, {data: "nodes", orderable: false}, {data: "system_users", orderable: false}, - {data: "is_valid", orderable: false}, {data: "id", orderable: false, width: "100px"} + {data: "is_valid", orderable: false}, {data: "id", orderable: false, width: "120px"} ], select: {}, op_html: $('#actions').html() @@ -213,6 +188,10 @@ $(document).ready(function(){ {title: "{% trans 'Hostname' %}", value: "hostname"}, {title: "{% trans 'Node' %}", value: "node"}, {title: "{% trans 'System user' %}", value: "system_user"}, + {title: "{% trans 'Inherit' %}", value: "all", submenu: [ + {title: "{% trans 'Include' %}", value: "1"}, + {title: "{% trans 'Exclude' %}", value: "0"}, + ]}, ]; initTableFilterDropdown('#permission_list_table_filter input', filterMenu) }) diff --git a/apps/perms/templates/perms/asset_permission_user.html b/apps/perms/templates/perms/asset_permission_user.html index f06c3409c..bb9ca375a 100644 --- a/apps/perms/templates/perms/asset_permission_user.html +++ b/apps/perms/templates/perms/asset_permission_user.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -48,29 +44,19 @@
    - +
    + - - + - {% for user in object_list %} - - - - - - {% endfor %}
    + + {% trans 'Name' %}{% trans 'Username' %}{% trans 'Action' %}
    {{ user.name }}{{ user.username }} - -
    -
    - {% include '_pagination.html' %} -
    @@ -85,10 +71,7 @@
    - @@ -113,7 +96,7 @@ - {% for user_group in user_groups_remain %} {% endfor %} @@ -128,7 +111,7 @@
    {% for user_group in asset_permission.user_groups.all %} - + {{ user_group }} @@ -145,120 +128,124 @@
    - {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/perms/templates/perms/database_app_permission_create_update.html b/apps/perms/templates/perms/database_app_permission_create_update.html new file mode 100644 index 000000000..a779e5826 --- /dev/null +++ b/apps/perms/templates/perms/database_app_permission_create_update.html @@ -0,0 +1,143 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% load bootstrap3 %} +{% block custom_head_css_js %} + +{% endblock %} + +{% block content %} +
    +
    +
    +
    +
    +
    {{ action }}
    + +
    +
    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} + +

    {% trans 'Basic' %}

    + {% bootstrap_field form.name layout="horizontal" %} +
    + +

    {% trans 'User' %}

    + {% bootstrap_field form.users layout="horizontal" %} + {% bootstrap_field form.user_groups layout="horizontal" %} +
    + +

    {% trans 'DatabaseApp' %}

    + {% bootstrap_field form.database_apps layout="horizontal" %} + {% bootstrap_field form.system_users layout="horizontal" %} +
    + +

    {% trans 'Other' %}

    +
    + +
    + {{ form.is_active }} +
    +
    +
    + +
    +
    + + {% if form.errors %} + + to + + {% else %} + + to + + {% endif %} +
    + {{ form.date_expired.errors }} + {{ form.date_start.errors }} +
    +
    + + {% bootstrap_field form.comment layout="horizontal" %} + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + + + + + + +{% endblock %} diff --git a/apps/perms/templates/perms/database_app_permission_database_app.html b/apps/perms/templates/perms/database_app_permission_database_app.html new file mode 100644 index 000000000..0a23618d0 --- /dev/null +++ b/apps/perms/templates/perms/database_app_permission_database_app.html @@ -0,0 +1,237 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% trans 'DatabaseApp list of ' %} {{ database_app_permission.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + +
    + + {% trans 'DatabaseApp' %}{% trans 'Action' %}
    +
    +
    +
    +
    +
    +
    + {% trans 'Add DatabaseApp to this permission' %} +
    +
    + + + + + + + + + + + +
    + +
    + +
    +
    +
    + +
    +
    + {% trans 'System user' %} +
    +
    + + + + + + + + + + + + {% for system_user in object.system_users.all %} + + + + + {% endfor %} + +
    + +
    + +
    {{ system_user|truncatechars:21}} + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/perms/templates/perms/database_app_permission_detail.html b/apps/perms/templates/perms/database_app_permission_detail.html new file mode 100644 index 000000000..feae4db24 --- /dev/null +++ b/apps/perms/templates/perms/database_app_permission_detail.html @@ -0,0 +1,157 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'User count' %}:{{ object.users.count }}
    {% trans 'User group count' %}:{{ object.user_groups.count }}
    {% trans 'DatabaseApp count' %}:{{ object.database_apps.count }}
    {% trans 'System user count' %}:{{ object.system_users.count }}
    {% trans 'Date start' %}:{{ object.date_start }}
    {% trans 'Date expired' %}:{{ object.date_expired }}
    {% trans 'Date created' %}:{{ object.date_created }}
    {% trans 'Created by' %}:{{ object.created_by }}
    {% trans 'Comment' %}:{{ object.comment }}
    +
    +
    +
    + +
    +
    +
    + {% trans 'Quick update' %} +
    +
    + + + + + + + +
    {% trans 'Active' %} : +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/perms/templates/perms/database_app_permission_list.html b/apps/perms/templates/perms/database_app_permission_list.html new file mode 100644 index 000000000..b85454cb8 --- /dev/null +++ b/apps/perms/templates/perms/database_app_permission_list.html @@ -0,0 +1,99 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block table_search %}{% endblock %} +{% block table_container %} + + + + + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'DatabaseApp' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/perms/templates/perms/database_app_permission_user.html b/apps/perms/templates/perms/database_app_permission_user.html new file mode 100644 index 000000000..603f60a6e --- /dev/null +++ b/apps/perms/templates/perms/database_app_permission_user.html @@ -0,0 +1,251 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% trans 'User list of ' %} {{ database_app_permission.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'Action' %}
    +
    +
    +
    +
    +
    +
    + {% trans 'Add user to permission' %} +
    +
    + + + + + + + + + + + +
    + +
    + +
    +
    +
    + +
    +
    + {% trans 'Add user group to permission' %} +
    +
    + + + + + + + + + + + + {% for user_group in database_app_permission.user_groups.all %} + + + + + {% endfor %} + +
    + +
    + +
    {{ user_group }} + +
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/perms/templates/perms/remote_app_permission_create_update.html b/apps/perms/templates/perms/remote_app_permission_create_update.html index b7652a59b..f6e0cda7d 100644 --- a/apps/perms/templates/perms/remote_app_permission_create_update.html +++ b/apps/perms/templates/perms/remote_app_permission_create_update.html @@ -3,8 +3,6 @@ {% load static %} {% load bootstrap3 %} {% block custom_head_css_js %} - - {% endblock %} @@ -125,7 +123,7 @@ $(document).ready(function () { var method = "POST"; var the_url = '{% url "api-perms:remote-app-permission-list" %}'; var redirect_to = '{% url "perms:remote-app-permission-list" %}'; - {% if type == "update" %} + {% if api_action == "update" %} the_url = '{% url "api-perms:remote-app-permission-detail" pk=object.id %}'; method = "PUT"; {% endif %} diff --git a/apps/perms/templates/perms/remote_app_permission_detail.html b/apps/perms/templates/perms/remote_app_permission_detail.html index f2ec8fa35..aedef832e 100644 --- a/apps/perms/templates/perms/remote_app_permission_detail.html +++ b/apps/perms/templates/perms/remote_app_permission_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -244,4 +239,4 @@ $(document).ready(function () { }); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/perms/templates/perms/remote_app_permission_list.html b/apps/perms/templates/perms/remote_app_permission_list.html index 3812b747b..f6071430a 100644 --- a/apps/perms/templates/perms/remote_app_permission_list.html +++ b/apps/perms/templates/perms/remote_app_permission_list.html @@ -75,7 +75,7 @@ function initTable() { {data: "remote_apps", orderable: false}, {data: "system_users", orderable: false}, {data: "is_valid", orderable: false}, - {data: "id", orderable: false} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/apps/perms/templates/perms/remote_app_permission_remote_app.html b/apps/perms/templates/perms/remote_app_permission_remote_app.html index 091613317..991489033 100644 --- a/apps/perms/templates/perms/remote_app_permission_remote_app.html +++ b/apps/perms/templates/perms/remote_app_permission_remote_app.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -161,4 +157,4 @@ removeRemoteApps(remote_apps) }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/perms/templates/perms/remote_app_permission_user.html b/apps/perms/templates/perms/remote_app_permission_user.html index e18e4f694..d222dfb7c 100644 --- a/apps/perms/templates/perms/remote_app_permission_user.html +++ b/apps/perms/templates/perms/remote_app_permission_user.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -253,4 +249,4 @@ $tr.remove() }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py index 5ffa0d14f..9ec3b754f 100644 --- a/apps/perms/urls/api_urls.py +++ b/apps/perms/urls/api_urls.py @@ -1,115 +1,20 @@ # coding:utf-8 -from django.urls import path, re_path -from rest_framework import routers +from django.urls import re_path from common import api as capi -from .. import api +from .asset_permission import asset_permission_urlpatterns +from .remote_app_permission import remote_app_permission_urlpatterns +from .database_app_permission import database_app_permission_urlpatterns app_name = 'perms' -router = routers.DefaultRouter() -router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission') -router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission') - - -asset_permission_urlpatterns = [ - # Assets - path('users//assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'), - path('users/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'), - - # Assets as tree - path('users//assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'), - path('users/assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='my-assets-as-tree'), - - # Nodes - path('users//nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), - path('users/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'), - - # Node children - path('users//nodes/children/', api.UserGrantedNodesApi.as_view(), name='user-nodes-children'), - path('users/nodes/children/', api.UserGrantedNodesApi.as_view(), name='my-nodes-children'), - - # Node as tree - path('users//nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), - path('users/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), - - # Node with assets as tree - path('users//nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-with-assets-as-tree'), - path('users/nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'), - - # Node children as tree - path('users//nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='user-nodes-children-as-tree'), - path('users/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'), - - # Node children with assets as tree - path('users//nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'), - path('users/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'), - - # Node assets - path('users//nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), - path('users/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'), - - # Asset System users - path('users//assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='user-asset-system-users'), - path('users/assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'), - - # 查询某个用户组授权的资产和资产组 - path('user-groups//assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), - path('user-groups//nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), - path('user-groups//nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'), - path('user-groups//nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'), - path('user-groups//nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'), - path('user-groups//assets//system-users/', api.UserGroupGrantedAssetSystemUsersApi.as_view(), name='user-group-asset-system-users'), - - # 用户和资产授权变更 - path('asset-permissions//users/remove/', api.AssetPermissionRemoveUserApi.as_view(), name='asset-permission-remove-user'), - path('asset-permissions//users/add/', api.AssetPermissionAddUserApi.as_view(), name='asset-permission-add-user'), - path('asset-permissions//assets/remove/', api.AssetPermissionRemoveAssetApi.as_view(), name='asset-permission-remove-asset'), - path('asset-permissions//assets/add/', api.AssetPermissionAddAssetApi.as_view(), name='asset-permission-add-asset'), - - # 授权规则中授权的资产 - path('asset-permissions//assets/', api.AssetPermissionAssetsApi.as_view(), name='asset-permission-assets'), - - # 验证用户是否有某个资产和系统用户的权限 - path('asset-permissions/user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), - path('asset-permissions/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), - - # 刷新缓存 - path('asset-permissions/cache/refresh/', api.RefreshAssetPermissionCacheApi.as_view(), name='refresh-asset-permission-cache'), -] - - -remote_app_permission_urlpatterns = [ - # 查询用户授权的RemoteApp - path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'), - path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'), - - # 获取用户授权的RemoteApp树 - path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'), - path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'), - - # 查询用户组授权的RemoteApp - path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'), - - # RemoteApp System users - path('users//remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='user-remote-app-system-users'), - path('users/remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='my-remote-app-system-users'), - - # 校验用户对RemoteApp的权限 - path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'), - - # 用户和RemoteApp变更 - path('remote-app-permissions//users/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'), - path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'), - path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'), - path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'), -] old_version_urlpatterns = [ re_path('(?Puser|user-group|asset-permission|remote-app-permission)/.*', capi.redirect_plural_name_api) ] -urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns + old_version_urlpatterns - -urlpatterns += router.urls +urlpatterns = asset_permission_urlpatterns + \ + remote_app_permission_urlpatterns + \ + database_app_permission_urlpatterns + \ + old_version_urlpatterns diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py new file mode 100644 index 000000000..4a649c992 --- /dev/null +++ b/apps/perms/urls/asset_permission.py @@ -0,0 +1,86 @@ +# coding:utf-8 + +from django.urls import path, include +from rest_framework_bulk.routes import BulkRouter +from .. import api + +router = BulkRouter() +router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission') +router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet, 'asset-permissions-users-relation') +router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet, 'asset-permissions-user-groups-relation') +router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet, 'asset-permissions-assets-relation') +router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, 'asset-permissions-nodes-relation') +router.register('asset-permissions-system-users-relations', api.AssetPermissionSystemUserRelationViewSet, 'asset-permissions-system-users-relation') + +user_permission_urlpatterns = [ + path('/assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'), + path('assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'), + + # Assets as tree + path('/assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'), + path('assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='my-assets-as-tree'), + + # Nodes + path('/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), + path('nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'), + + # Node children + path('/nodes/children/', api.UserGrantedNodesApi.as_view(), name='user-nodes-children'), + path('nodes/children/', api.UserGrantedNodesApi.as_view(), name='my-nodes-children'), + + # Node as tree + path('/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), + path('nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), + + # Node with assets as tree + path('/nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-with-assets-as-tree'), + path('nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'), + + # Node children as tree + path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='user-nodes-children-as-tree'), + path('nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'), + + # Node children with assets as tree + path('/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'), + path('nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'), + + # Node assets + path('/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), + path('nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'), + + # Asset System users + path('/assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='user-asset-system-users'), + path('assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'), +] + +user_group_permission_urlpatterns = [ + # 查询某个用户组授权的资产和资产组 + path('/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), + path('/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), + path('/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'), + path('/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'), + path('/nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'), + path('/assets//system-users/', api.UserGroupGrantedAssetSystemUsersApi.as_view(), name='user-group-asset-system-users'), +] + +permission_urlpatterns = [ + # 授权规则中授权的资产 + path('/assets/all/', api.AssetPermissionAllAssetListApi.as_view(), name='asset-permission-all-assets'), + path('/users/all/', api.AssetPermissionAllUserListApi.as_view(), name='asset-permission-all-users'), + + # 验证用户是否有某个资产和系统用户的权限 + path('user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), + path('user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), + + # 刷新缓存 + path('cache/refresh/', api.RefreshAssetPermissionCacheApi.as_view(), name='refresh-asset-permission-cache'), +] + +asset_permission_urlpatterns = [ + # Assets + path('users/', include(user_permission_urlpatterns)), + path('user-groups/', include(user_group_permission_urlpatterns)), + path('asset-permissions/', include(permission_urlpatterns)), +] + +asset_permission_urlpatterns += router.urls diff --git a/apps/perms/urls/database_app_permission.py b/apps/perms/urls/database_app_permission.py new file mode 100644 index 000000000..a793f980e --- /dev/null +++ b/apps/perms/urls/database_app_permission.py @@ -0,0 +1,47 @@ +# coding: utf-8 +# + +from django.urls import path, include +from rest_framework_bulk.routes import BulkRouter +from .. import api + + +router = BulkRouter() +router.register('database-app-permissions', api.DatabaseAppPermissionViewSet, 'database-app-permission') +router.register('database-app-permissions-users-relations', api.DatabaseAppPermissionUserRelationViewSet, 'database-app-permissions-users-relation') +router.register('database-app-permissions-user-groups-relations', api.DatabaseAppPermissionUserGroupRelationViewSet, 'database-app-permissions-user-groups-relation') +router.register('database-app-permissions-database-apps-relations', api.DatabaseAppPermissionDatabaseAppRelationViewSet, 'database-app-permissions-database-apps-relation') +router.register('database-app-permissions-system-users-relations', api.DatabaseAppPermissionSystemUserRelationViewSet, 'database-app-permissions-system-users-relation') + +user_permission_urlpatterns = [ + path('/database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='user-database-apps'), + path('database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='my-database-apps'), + + # DatabaseApps as tree + path('/database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='user-databases-apps-tree'), + path('database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='my-databases-apps-tree'), + + path('/database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'), + path('database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'), +] + +user_group_permission_urlpatterns = [ + path('/database-apps/', api.UserGroupGrantedDatabaseAppsApi.as_view(), name='user-group-database-apps'), +] + +permission_urlpatterns = [ + # 授权规则中授权的用户和数据库应用 + path('/users/all/', api.DatabaseAppPermissionAllUserListApi.as_view(), name='database-app-permission-all-users'), + path('/database-apps/all/', api.DatabaseAppPermissionAllDatabaseAppListApi.as_view(), name='database-app-permission-all-database-apps'), + + # 验证用户是否有某个数据库应用的权限 + path('user/validate/', api.ValidateUserDatabaseAppPermissionApi.as_view(), name='validate-user-database-app-permission'), +] + +database_app_permission_urlpatterns = [ + path('users/', include(user_permission_urlpatterns)), + path('user-groups/', include(user_group_permission_urlpatterns)), + path('database-app-permissions/', include(permission_urlpatterns)) +] + +database_app_permission_urlpatterns += router.urls diff --git a/apps/perms/urls/remote_app_permission.py b/apps/perms/urls/remote_app_permission.py new file mode 100644 index 000000000..8f83d72d0 --- /dev/null +++ b/apps/perms/urls/remote_app_permission.py @@ -0,0 +1,38 @@ +# coding:utf-8 + +from django.urls import path +from rest_framework_bulk.routes import BulkRouter +from .. import api + + +router = BulkRouter() +router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission') + +remote_app_permission_urlpatterns = [ + # 查询用户授权的RemoteApp + path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'), + path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'), + + # 获取用户授权的RemoteApp树 + path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'), + path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'), + + # 查询用户组授权的RemoteApp + path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'), + + # RemoteApp System users + path('users//remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='user-remote-app-system-users'), + path('users/remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='my-remote-app-system-users'), + + # 校验用户对RemoteApp的权限 + path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'), + + # 用户和RemoteApp变更 + path('remote-app-permissions//users/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'), + path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'), + path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'), + path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'), +] + +remote_app_permission_urlpatterns += router.urls + diff --git a/apps/perms/urls/views_urls.py b/apps/perms/urls/views_urls.py index 964025db3..f4deba8c8 100644 --- a/apps/perms/urls/views_urls.py +++ b/apps/perms/urls/views_urls.py @@ -23,4 +23,12 @@ urlpatterns = [ path('remote-app-permission//', views.RemoteAppPermissionDetailView.as_view(), name='remote-app-permission-detail'), path('remote-app-permission//user/', views.RemoteAppPermissionUserView.as_view(), name='remote-app-permission-user-list'), path('remote-app-permission//remote-app/', views.RemoteAppPermissionRemoteAppView.as_view(), name='remote-app-permission-remote-app-list'), + + # database-app-permission + path('database-app-permission/', views.DatabaseAppPermissionListView.as_view(), name='database-app-permission-list'), + path('database-app-permission/create/', views.DatabaseAppPermissionCreateView.as_view(), name='database-app-permission-create'), + path('database-app-permission//update/', views.DatabaseAppPermissionUpdateView.as_view(), name='database-app-permission-update'), + path('database-app-permission//', views.DatabaseAppPermissionDetailView.as_view(), name='database-app-permission-detail'), + path('database-app-permission//user/', views.DatabaseAppPermissionUserView.as_view(), name='database-app-permission-user-list'), + path('database-app-permission//database-app/', views.DatabaseAppPermissionDatabaseAppView.as_view(), name='database-app-permission-database-app-list'), ] diff --git a/apps/perms/utils/__init__.py b/apps/perms/utils/__init__.py index 129901afc..c6581b858 100644 --- a/apps/perms/utils/__init__.py +++ b/apps/perms/utils/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index f71f83663..27c839d59 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -437,7 +437,7 @@ def sort_assets(assets, order_by='hostname', reverse=False): class ParserNode: nodes_only_fields = ("key", "value", "id") - assets_only_fields = ("platform", "hostname", "id", "ip", "protocols") + assets_only_fields = ("hostname", "id", "ip", "protocols", "org_id") system_users_only_fields = ( "id", "name", "username", "protocol", "priority", "login_mode", ) @@ -445,7 +445,6 @@ class ParserNode: @staticmethod def parse_node_to_tree_node(node): name = '{} ({})'.format(node.value, node.assets_amount) - # name = node.value data = { 'id': node.key, 'name': name, @@ -468,7 +467,7 @@ class ParserNode: @staticmethod def parse_asset_to_tree_node(node, asset): icon_skin = 'file' - platform = asset.platform.lower() + platform = asset.platform_base.lower() if platform == 'windows': icon_skin = 'windows' elif platform == 'linux': @@ -489,8 +488,8 @@ class ParserNode: 'hostname': asset.hostname, 'ip': asset.ip, 'protocols': asset.protocols_as_list, - 'platform': asset.platform, - "org_name": asset.org_name, + 'platform': asset.platform_base, + 'org_name': asset.org_name, }, } } diff --git a/apps/perms/utils/database_app_permission.py b/apps/perms/utils/database_app_permission.py new file mode 100644 index 000000000..526b0fc59 --- /dev/null +++ b/apps/perms/utils/database_app_permission.py @@ -0,0 +1,99 @@ +# coding: utf-8 +# + +from django.db.models import Q +from orgs.utils import set_to_root_org + +from ..models import DatabaseAppPermission +from common.tree import TreeNode +from applications.models import DatabaseApp +from assets.models import SystemUser + + +__all__ = [ + 'DatabaseAppPermissionUtil', + 'construct_database_apps_tree_root', + 'parse_database_app_to_tree_node' +] + + +def get_user_database_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 DatabaseAppPermission.objects.all().valid().filter(arg) + + +def get_user_group_database_app_permission(user_group): + return DatabaseAppPermission.objects.all().valid().filter( + user_group=user_group + ) + + +class DatabaseAppPermissionUtil: + get_permissions_map = { + 'User': get_user_database_app_permissions, + 'UserGroup': get_user_group_database_app_permission + } + + def __init__(self, obj): + self.object = obj + self.change_org_if_need() + + @staticmethod + def change_org_if_need(): + set_to_root_org() + + @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_database_apps(self): + database_apps = DatabaseApp.objects.filter( + granted_by_permissions__in=self.permissions + ) + return database_apps + + def get_database_app_system_users(self, database_app): + queryset = self.permissions + kwargs = {'database_apps': database_app} + queryset = queryset.filter(**kwargs) + system_users_ids = queryset.values_list('system_users', flat=True) + system_users_ids = system_users_ids.distinct() + system_users = SystemUser.objects.filter(id__in=system_users_ids) + system_users = system_users.order_by('-priority') + return system_users + + +def construct_database_apps_tree_root(): + tree_root = { + 'id': 'ID_DATABASE_APP_ROOT', + 'name': 'DatabaseApp', + 'title': 'DatabaseApp', + 'pId': '', + 'open': False, + 'isParent': True, + 'iconSkin': '', + 'meta': {'type': 'database_app'} + } + return TreeNode(**tree_root) + + +def parse_database_app_to_tree_node(parent, database_app): + pid = parent.id if parent else '' + tree_node = { + 'id': database_app.id, + 'name': database_app.name, + 'title': database_app.name, + 'pId': pid, + 'open': False, + 'isParent': False, + 'iconSkin': 'file', + 'meta': {'type': 'database_app'} + } + return TreeNode(**tree_node) diff --git a/apps/perms/views/__init__.py b/apps/perms/views/__init__.py index 129901afc..c6581b858 100644 --- a/apps/perms/views/__init__.py +++ b/apps/perms/views/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/apps/perms/views/asset_permission.py b/apps/perms/views/asset_permission.py index 6130ad2e3..71b4d5604 100644 --- a/apps/perms/views/asset_permission.py +++ b/apps/perms/views/asset_permission.py @@ -98,9 +98,7 @@ class AssetPermissionDetailView(PermissionsMixin, DetailView): context = { 'app': _('Perms'), 'action': _('Asset permission detail'), - 'system_users_remain': SystemUser.objects.exclude( - granted_by_permissions=self.object - ), + } kwargs.update(context) return super().get_context_data(**kwargs) @@ -131,14 +129,13 @@ class AssetPermissionUserView(PermissionsMixin, return queryset def get_context_data(self, **kwargs): - user_remain = current_org.get_org_members(exclude=('Auditor',)).exclude( - assetpermission=self.object) + users = [str(i) for i in self.object.users.all().values_list('id', flat=True)] user_groups_remain = UserGroup.objects.exclude( assetpermission=self.object) context = { 'app': _('Perms'), 'action': _('Asset permission user list'), - 'users_remain': user_remain, + 'users': users, 'user_groups_remain': user_groups_remain, } kwargs.update(context) @@ -163,13 +160,16 @@ class AssetPermissionAssetView(PermissionsMixin, return queryset def get_context_data(self, **kwargs): - nodes_remain = Node.objects.exclude( - id__in=self.object.nodes.all().values_list('id', flat=True) - ).only('key') + assets = self.object.assets.all().values_list('id', flat=True) + assets = [str(i) for i in assets] + system_users_remain = SystemUser.objects\ + .exclude(granted_by_permissions=self.object)\ + .exclude(protocol=SystemUser.PROTOCOL_MYSQL) context = { 'app': _('Perms'), + 'assets': assets, 'action': _('Asset permission asset list'), - 'nodes_remain': nodes_remain, + 'system_users_remain': system_users_remain, } kwargs.update(context) - return super().get_context_data(**kwargs) \ No newline at end of file + return super().get_context_data(**kwargs) diff --git a/apps/perms/views/database_app_permission.py b/apps/perms/views/database_app_permission.py new file mode 100644 index 000000000..50627defe --- /dev/null +++ b/apps/perms/views/database_app_permission.py @@ -0,0 +1,152 @@ +# coding: utf-8 +# + +from django.utils.translation import ugettext as _ + +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 PermissionsMixin, IsOrgAdmin +from users.models import UserGroup +from applications.models import DatabaseApp +from assets.models import SystemUser + +from .. import models, forms + + +__all__ = [ + 'DatabaseAppPermissionListView', 'DatabaseAppPermissionCreateView', + 'DatabaseAppPermissionUpdateView', 'DatabaseAppPermissionDetailView', + 'DatabaseAppPermissionUserView', 'DatabaseAppPermissionDatabaseAppView', +] + + +class DatabaseAppPermissionListView(PermissionsMixin, TemplateView): + template_name = 'perms/database_app_permission_list.html' + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('DatabaseApp permission list') + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionCreateView(PermissionsMixin, CreateView): + template_name = 'perms/database_app_permission_create_update.html' + model = models.DatabaseAppPermission + form_class = forms.DatabaseAppPermissionCreateUpdateForm + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('Create DatabaseApp permission'), + 'api_action': 'create', + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionUpdateView(PermissionsMixin, UpdateView): + template_name = 'perms/database_app_permission_create_update.html' + model = models.DatabaseAppPermission + form_class = forms.DatabaseAppPermissionCreateUpdateForm + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('Update DatabaseApp permission'), + 'api_action': 'update' + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionDetailView(PermissionsMixin, DetailView): + template_name = 'perms/database_app_permission_detail.html' + model = models.DatabaseAppPermission + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('DatabaseApp permission detail') + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionUserView(PermissionsMixin, + SingleObjectMixin, + ListView): + template_name = 'perms/database_app_permission_user.html' + context_object_name = 'database_app_permission' + paginate_by = settings.DISPLAY_PER_PAGE + object = None + permission_classes = [IsOrgAdmin] + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=models.DatabaseAppPermission.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): + users = [str(i) for i in self.object.users.all().values_list('id', flat=True)] + user_groups_remain = UserGroup.objects.exclude( + databaseapppermission=self.object) + context = { + 'app': _('Perms'), + 'action': _('DatabaseApp permission user list'), + 'users': users, + 'user_groups_remain': user_groups_remain, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionDatabaseAppView(PermissionsMixin, + SingleObjectMixin, + ListView): + template_name = 'perms/database_app_permission_database_app.html' + context_object_name = 'database_app_permission' + paginate_by = settings.DISPLAY_PER_PAGE + object = None + permission_classes = [IsOrgAdmin] + + def get(self, request, *args, **kwargs): + self.object = self.get_object( + queryset=models.DatabaseAppPermission.objects.all() + ) + return super().get(request, *args, **kwargs) + + def get_queryset(self): + queryset = list(self.object.get_all_database_apps()) + return queryset + + def get_context_data(self, **kwargs): + database_apps = self.object.get_all_database_apps().values_list('id', flat=True) + database_apps = [str(i) for i in database_apps] + system_users_remain = SystemUser.objects\ + .exclude(granted_by_database_app_permissions=self.object)\ + .filter(protocol=SystemUser.PROTOCOL_MYSQL) + context = { + 'app': _('Perms'), + 'database_apps': database_apps, + 'database_apps_remain': DatabaseApp.objects.exclude( + granted_by_permissions=self.object + ), + 'system_users_remain': system_users_remain, + 'action': _('DatabaseApp permission DatabaseApp list'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) diff --git a/apps/perms/views/remote_app_permission.py b/apps/perms/views/remote_app_permission.py index 742a5fd85..875600c68 100644 --- a/apps/perms/views/remote_app_permission.py +++ b/apps/perms/views/remote_app_permission.py @@ -48,7 +48,7 @@ class RemoteAppPermissionCreateView(PermissionsMixin, CreateView): context = { 'app': _('Perms'), 'action': _('Create RemoteApp permission'), - 'type': 'create' + 'api_action': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -65,7 +65,7 @@ class RemoteAppPermissionUpdateView(PermissionsMixin, UpdateView): context = { 'app': _('Perms'), 'action': _('Update RemoteApp permission'), - 'type': 'update' + 'api_action': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -77,12 +77,13 @@ class RemoteAppPermissionDetailView(PermissionsMixin, DetailView): permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): + system_users_remain = SystemUser.objects\ + .exclude(granted_by_remote_app_permissions=self.object)\ + .filter(protocol=SystemUser.PROTOCOL_RDP) context = { 'app': _('Perms'), 'action': _('RemoteApp permission detail'), - 'system_users_remain': SystemUser.objects.exclude( - granted_by_remote_app_permissions=self.object - ), + 'system_users_remain': system_users_remain, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -107,10 +108,10 @@ class RemoteAppPermissionUserView(PermissionsMixin, return queryset def get_context_data(self, **kwargs): - user_remain = current_org.get_org_members(exclude=('Auditor',)).exclude( - remoteapppermission=self.object) - user_groups_remain = UserGroup.objects.exclude( - remoteapppermission=self.object) + user_remain = current_org.get_org_members(exclude=('Auditor',))\ + .exclude(remoteapppermission=self.object) + user_groups_remain = UserGroup.objects\ + .exclude(remoteapppermission=self.object) context = { 'app': _('Perms'), 'action': _('RemoteApp permission user list'), diff --git a/apps/settings/api.py b/apps/settings/api.py index 1c65dc12c..354cbdd1d 100644 --- a/apps/settings/api.py +++ b/apps/settings/api.py @@ -245,87 +245,6 @@ class LDAPCacheRefreshAPI(generics.RetrieveAPIView): return Response(data={'msg': 'success'}) -class ReplayStorageCreateAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_data = request.data - - if storage_data.get('TYPE') == 'ceph': - port = storage_data.get('PORT') - if port.isdigit(): - storage_data['PORT'] = int(storage_data.get('PORT')) - - storage_name = storage_data.pop('NAME') - data = {storage_name: storage_data} - - if not self.is_valid(storage_data): - return Response({ - "error": _("Error: Account invalid (Please make sure the " - "information such as Access key or Secret key is correct)")}, - status=401 - ) - - Setting.save_storage('TERMINAL_REPLAY_STORAGE', data) - return Response({"msg": _('Create succeed')}, status=200) - - @staticmethod - def is_valid(storage_data): - if storage_data.get('TYPE') == 'server': - return True - storage = jms_storage.get_object_storage(storage_data) - target = 'tests.py' - src = os.path.join(settings.BASE_DIR, 'common', target) - return storage.is_valid(src, target) - - -class ReplayStorageDeleteAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_name = str(request.data.get('name')) - Setting.delete_storage('TERMINAL_REPLAY_STORAGE', storage_name) - return Response({"msg": _('Delete succeed')}, status=200) - - -class CommandStorageCreateAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_data = request.data - storage_name = storage_data.pop('NAME') - data = {storage_name: storage_data} - if not self.is_valid(storage_data): - return Response( - {"error": _("Error: Account invalid (Please make sure the " - "information such as Access key or Secret key is correct)")}, - status=401 - ) - - Setting.save_storage('TERMINAL_COMMAND_STORAGE', data) - return Response({"msg": _('Create succeed')}, status=200) - - @staticmethod - def is_valid(storage_data): - if storage_data.get('TYPE') == 'server': - return True - try: - storage = jms_storage.get_log_storage(storage_data) - except Exception: - return False - - return storage.ping() - - -class CommandStorageDeleteAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_name = str(request.data.get('name')) - Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name) - return Response({"msg": _('Delete succeed')}, status=200) - - class PublicSettingApi(generics.RetrieveAPIView): permission_classes = () serializer_class = PublicSettingSerializer diff --git a/apps/settings/forms.py b/apps/settings/forms.py deleted file mode 100644 index 5b55091cb..000000000 --- a/apps/settings/forms.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- -# -import json -from django import forms -from django.utils.translation import ugettext_lazy as _ -from django.db import transaction - -from .models import Setting, settings -from common.fields import ( - FormDictField, FormEncryptCharField, FormEncryptMixin -) - - -class BaseForm(forms.Form): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for name, field in self.fields.items(): - value = getattr(settings, name, None) - if value is None: # and django_value is None: - continue - - if value is not None: - if isinstance(value, dict): - value = json.dumps(value) - initial_value = value - else: - initial_value = '' - field.initial = initial_value - - def save(self, category="default"): - if not self.is_bound: - raise ValueError("Form is not bound") - - # db_settings = Setting.objects.all() - if not self.is_valid(): - raise ValueError(self.errors) - - with transaction.atomic(): - for name, value in self.cleaned_data.items(): - field = self.fields[name] - if isinstance(field.widget, forms.PasswordInput) and not value: - continue - # if value == getattr(settings, name): - # continue - - encrypted = True if isinstance(field, FormEncryptMixin) else False - try: - setting = Setting.objects.get(name=name) - except Setting.DoesNotExist: - setting = Setting() - setting.name = name - setting.category = category - setting.encrypted = encrypted - setting.cleaned_value = value - setting.save() - - -class BasicSettingForm(BaseForm): - SITE_URL = forms.URLField( - label=_("Current SITE URL"), - help_text="eg: http://jumpserver.abc.com:8080" - ) - USER_GUIDE_URL = forms.URLField( - label=_("User Guide URL"), required=False, - help_text=_("User first login update profile done redirect to it") - ) - EMAIL_SUBJECT_PREFIX = forms.CharField( - max_length=1024, label=_("Email Subject Prefix"), - help_text=_("Tips: Some word will be intercept by mail provider") - ) - - -class EmailSettingForm(BaseForm): - EMAIL_HOST = forms.CharField( - max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org' - ) - EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25) - EMAIL_HOST_USER = forms.CharField( - max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org' - ) - EMAIL_HOST_PASSWORD = FormEncryptCharField( - max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput, - required=False, - help_text=_("Tips: Some provider use token except password") - ) - EMAIL_FROM = forms.CharField( - max_length=128, label=_("Send user"), initial='', required=False, - help_text=_( - "Tips: Send mail account, default SMTP account as the send account" - ) - ) - EMAIL_RECIPIENT = forms.CharField( - max_length=128, label=_("Test recipient"), initial='', required=False, - help_text=_("Tips: Used only as a test mail recipient") - ) - EMAIL_USE_SSL = forms.BooleanField( - label=_("Use SSL"), initial=False, required=False, - help_text=_("If SMTP port is 465, may be select") - ) - EMAIL_USE_TLS = forms.BooleanField( - label=_("Use TLS"), initial=False, required=False, - help_text=_("If SMTP port is 587, may be select") - ) - - -class LDAPSettingForm(BaseForm): - AUTH_LDAP_SERVER_URI = forms.CharField( - label=_("LDAP server"), - ) - AUTH_LDAP_BIND_DN = forms.CharField( - required=False, label=_("Bind DN"), - ) - AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField( - label=_("Password"), - widget=forms.PasswordInput, required=False - ) - AUTH_LDAP_SEARCH_OU = forms.CharField( - label=_("User OU"), - help_text=_("Use | split User OUs"), - required=False, - ) - AUTH_LDAP_SEARCH_FILTER = forms.CharField( - label=_("User search filter"), - help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)") - ) - AUTH_LDAP_USER_ATTR_MAP = FormDictField( - label=_("User attr map"), - help_text=_( - "User attr map present how to map LDAP user attr to jumpserver, " - "username,name,email is jumpserver attr" - ), - ) - # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU - # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER - # AUTH_LDAP_START_TLS = forms.BooleanField( - # label=_("Use SSL"), required=False - # ) - AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False) - - -class TerminalSettingForm(BaseForm): - SORT_BY_CHOICES = ( - ('hostname', _('Hostname')), - ('ip', _('IP')), - ) - PAGE_SIZE_CHOICES = ( - ('all', _('All')), - ('auto', _('Auto')), - (10, 10), - (15, 15), - (25, 25), - (50, 50), - ) - TERMINAL_PASSWORD_AUTH = forms.BooleanField( - required=False, label=_("Password auth") - ) - TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( - required=False, label=_("Public key auth") - ) - TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField( - min_value=5, max_value=99999, label=_("Heartbeat interval"), - help_text=_("Units: seconds") - ) - TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( - choices=SORT_BY_CHOICES, label=_("List sort by") - ) - TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField( - choices=PAGE_SIZE_CHOICES, label=_("List page size"), - ) - TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField( - min_value=1, max_value=99999, label=_("Session keep duration"), - help_text=_("Units: days, Session, record, command will be delete " - "if more than duration, only in database") - ) - TERMINAL_TELNET_REGEX = forms.CharField( - required=False, label=_("Telnet login regex"), - help_text=_("ex: Last\s*login|success|成功") - ) - - -class TerminalCommandStorage(BaseForm): - pass - - -class SecuritySettingForm(BaseForm): - # MFA global setting - SECURITY_MFA_AUTH = forms.BooleanField( - required=False, label=_("MFA Secondary certification"), - help_text=_( - 'After opening, the user login must use MFA secondary ' - 'authentication (valid for all users, including administrators)' - ) - ) - # Execute commands for user - SECURITY_COMMAND_EXECUTION = forms.BooleanField( - required=False, label=_("Batch execute commands"), - help_text=_("Allow user batch execute commands") - ) - SECURITY_SERVICE_ACCOUNT_REGISTRATION = forms.BooleanField( - required=False, label=_("Service account registration"), - help_text=_("Allow using bootstrap token register service account, " - "when terminal setup, can disable it") - ) - # limit login count - SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( - min_value=3, max_value=99999, - label=_("Limit the number of login failures") - ) - # limit login time - SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField( - min_value=5, max_value=99999, label=_("No logon interval"), - help_text=_( - "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." - ) - ) - # ssh max idle time - SECURITY_MAX_IDLE_TIME = forms.IntegerField( - min_value=1, max_value=99999, required=False, - label=_("Connection max idle time"), - help_text=_( - 'If idle time more than it, disconnect connection ' - 'Unit: minute' - ), - ) - # password expiration time - SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField( - min_value=1, max_value=99999, label=_("Password expiration time"), - help_text=_( - "Tip: (unit: day) " - "If the user does not update the password during the time, " - "the user password will expire failure;" - "The password expiration reminder mail will be automatic sent to the user " - "by system within 5 days (daily) before the password expires" - ) - ) - # min length - SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( - min_value=6, max_value=30, label=_("Password minimum length"), - ) - # upper case - SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField( - required=False, label=_("Must contain capital letters"), - help_text=_( - 'After opening, the user password changes ' - 'and resets must contain uppercase letters') - ) - # lower case - SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField( - required=False, label=_("Must contain lowercase letters"), - help_text=_('After opening, the user password changes ' - 'and resets must contain lowercase letters') - ) - # number - SECURITY_PASSWORD_NUMBER = forms.BooleanField( - required=False, label=_("Must contain numeric characters"), - help_text=_('After opening, the user password changes ' - 'and resets must contain numeric characters') - ) - # special char - SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField( - required=False, label=_("Must contain special characters"), - help_text=_('After opening, the user password changes ' - '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)") - ) - - diff --git a/apps/settings/forms/__init__.py b/apps/settings/forms/__init__.py new file mode 100644 index 000000000..4c5a69e8c --- /dev/null +++ b/apps/settings/forms/__init__.py @@ -0,0 +1,9 @@ +# coding: utf-8 +# + +from .base import * +from .basic import * +from .email import * +from .ldap import * +from .security import * +from .terminal import * diff --git a/apps/settings/forms/base.py b/apps/settings/forms/base.py new file mode 100644 index 000000000..b7b32dea6 --- /dev/null +++ b/apps/settings/forms/base.py @@ -0,0 +1,57 @@ +# coding: utf-8 +# + +import json +from django import forms +from django.db import transaction +from django.conf import settings + +from ..models import Setting +from common.fields import FormEncryptMixin + +__all__ = ['BaseForm'] + + +class BaseForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for name, field in self.fields.items(): + value = getattr(settings, name, None) + if value is None: # and django_value is None: + continue + + if value is not None: + if isinstance(value, dict): + value = json.dumps(value) + initial_value = value + else: + initial_value = '' + field.initial = initial_value + + def save(self, category="default"): + if not self.is_bound: + raise ValueError("Form is not bound") + + # db_settings = Setting.objects.all() + if not self.is_valid(): + raise ValueError(self.errors) + + with transaction.atomic(): + for name, value in self.cleaned_data.items(): + field = self.fields[name] + if isinstance(field.widget, forms.PasswordInput) and not value: + continue + # if value == getattr(settings, name): + # continue + + encrypted = True if isinstance(field, FormEncryptMixin) else False + try: + setting = Setting.objects.get(name=name) + except Setting.DoesNotExist: + setting = Setting() + setting.name = name + setting.category = category + setting.encrypted = encrypted + setting.cleaned_value = value + setting.save() + diff --git a/apps/settings/forms/basic.py b/apps/settings/forms/basic.py new file mode 100644 index 000000000..dd93c9537 --- /dev/null +++ b/apps/settings/forms/basic.py @@ -0,0 +1,24 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from .base import BaseForm + +__all__ = ['BasicSettingForm'] + + +class BasicSettingForm(BaseForm): + SITE_URL = forms.URLField( + label=_("Current SITE URL"), + help_text="eg: http://jumpserver.abc.com:8080" + ) + USER_GUIDE_URL = forms.URLField( + label=_("User Guide URL"), required=False, + help_text=_("User first login update profile done redirect to it") + ) + EMAIL_SUBJECT_PREFIX = forms.CharField( + max_length=1024, label=_("Email Subject Prefix"), + help_text=_("Tips: Some word will be intercept by mail provider") + ) + diff --git a/apps/settings/forms/email.py b/apps/settings/forms/email.py new file mode 100644 index 000000000..6fa61148a --- /dev/null +++ b/apps/settings/forms/email.py @@ -0,0 +1,65 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from common.fields import FormEncryptCharField +from .base import BaseForm + +__all__ = ['EmailSettingForm', 'EmailContentSettingForm'] + + +class EmailSettingForm(BaseForm): + EMAIL_HOST = forms.CharField( + max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org' + ) + EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25) + EMAIL_HOST_USER = forms.CharField( + max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org' + ) + EMAIL_HOST_PASSWORD = FormEncryptCharField( + max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput, + required=False, + help_text=_("Tips: Some provider use token except password") + ) + EMAIL_FROM = forms.CharField( + max_length=128, label=_("Send user"), initial='', required=False, + help_text=_( + "Tips: Send mail account, default SMTP account as the send account" + ) + ) + EMAIL_RECIPIENT = forms.CharField( + max_length=128, label=_("Test recipient"), initial='', required=False, + help_text=_("Tips: Used only as a test mail recipient") + ) + EMAIL_USE_SSL = forms.BooleanField( + label=_("Use SSL"), initial=False, required=False, + help_text=_("If SMTP port is 465, may be select") + ) + EMAIL_USE_TLS = forms.BooleanField( + label=_("Use TLS"), initial=False, required=False, + help_text=_("If SMTP port is 587, may be select") + ) + + +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)") + ) diff --git a/apps/settings/forms/ldap.py b/apps/settings/forms/ldap.py new file mode 100644 index 000000000..c44d1c3e4 --- /dev/null +++ b/apps/settings/forms/ldap.py @@ -0,0 +1,46 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from common.fields import FormDictField, FormEncryptCharField +from .base import BaseForm + + +__all__ = ['LDAPSettingForm'] + + +class LDAPSettingForm(BaseForm): + AUTH_LDAP_SERVER_URI = forms.CharField( + label=_("LDAP server"), + ) + AUTH_LDAP_BIND_DN = forms.CharField( + required=False, label=_("Bind DN"), + ) + AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField( + label=_("Password"), + widget=forms.PasswordInput, required=False + ) + AUTH_LDAP_SEARCH_OU = forms.CharField( + label=_("User OU"), + help_text=_("Use | split User OUs"), + required=False, + ) + AUTH_LDAP_SEARCH_FILTER = forms.CharField( + label=_("User search filter"), + help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)") + ) + AUTH_LDAP_USER_ATTR_MAP = FormDictField( + label=_("User attr map"), + help_text=_( + "User attr map present how to map LDAP user attr to jumpserver, " + "username,name,email is jumpserver attr" + ), + ) + # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU + # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER + # AUTH_LDAP_START_TLS = forms.BooleanField( + # label=_("Use SSL"), required=False + # ) + AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False) diff --git a/apps/settings/forms/security.py b/apps/settings/forms/security.py new file mode 100644 index 000000000..7b7cc247d --- /dev/null +++ b/apps/settings/forms/security.py @@ -0,0 +1,93 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .base import BaseForm + + +__all__ = ['SecuritySettingForm'] + + +class SecuritySettingForm(BaseForm): + # MFA global setting + SECURITY_MFA_AUTH = forms.BooleanField( + required=False, label=_("MFA Secondary certification"), + help_text=_( + 'After opening, the user login must use MFA secondary ' + 'authentication (valid for all users, including administrators)' + ) + ) + # Execute commands for user + SECURITY_COMMAND_EXECUTION = forms.BooleanField( + required=False, label=_("Batch execute commands"), + help_text=_("Allow user batch execute commands") + ) + SECURITY_SERVICE_ACCOUNT_REGISTRATION = forms.BooleanField( + required=False, label=_("Service account registration"), + help_text=_("Allow using bootstrap token register service account, " + "when terminal setup, can disable it") + ) + # limit login count + SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( + min_value=3, max_value=99999, + label=_("Limit the number of login failures") + ) + # limit login time + SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField( + min_value=5, max_value=99999, label=_("No logon interval"), + help_text=_( + "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." + ) + ) + # ssh max idle time + SECURITY_MAX_IDLE_TIME = forms.IntegerField( + min_value=1, max_value=99999, required=False, + label=_("Connection max idle time"), + help_text=_( + 'If idle time more than it, disconnect connection ' + 'Unit: minute' + ), + ) + # password expiration time + SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField( + min_value=1, max_value=99999, label=_("Password expiration time"), + help_text=_( + "Tip: (unit: day) " + "If the user does not update the password during the time, " + "the user password will expire failure;" + "The password expiration reminder mail will be automatic sent to the user " + "by system within 5 days (daily) before the password expires" + ) + ) + # min length + SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( + min_value=6, max_value=30, label=_("Password minimum length"), + ) + # upper case + SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField( + required=False, label=_("Must contain capital letters"), + help_text=_( + 'After opening, the user password changes ' + 'and resets must contain uppercase letters') + ) + # lower case + SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField( + required=False, label=_("Must contain lowercase letters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain lowercase letters') + ) + # number + SECURITY_PASSWORD_NUMBER = forms.BooleanField( + required=False, label=_("Must contain numeric characters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain numeric characters') + ) + # special char + SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField( + required=False, label=_("Must contain special characters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain special characters') + ) diff --git a/apps/settings/forms/terminal.py b/apps/settings/forms/terminal.py new file mode 100644 index 000000000..d879e88d2 --- /dev/null +++ b/apps/settings/forms/terminal.py @@ -0,0 +1,50 @@ +# coding: utf-8 +# + + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .base import BaseForm + +__all__ = ['TerminalSettingForm'] + + +class TerminalSettingForm(BaseForm): + SORT_BY_CHOICES = ( + ('hostname', _('Hostname')), + ('ip', _('IP')), + ) + PAGE_SIZE_CHOICES = ( + ('all', _('All')), + ('auto', _('Auto')), + (10, 10), + (15, 15), + (25, 25), + (50, 50), + ) + TERMINAL_PASSWORD_AUTH = forms.BooleanField( + required=False, label=_("Password auth") + ) + TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( + required=False, label=_("Public key auth") + ) + TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField( + min_value=5, max_value=99999, label=_("Heartbeat interval"), + help_text=_("Units: seconds") + ) + TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( + choices=SORT_BY_CHOICES, label=_("List sort by") + ) + TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField( + choices=PAGE_SIZE_CHOICES, label=_("List page size"), + ) + TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField( + min_value=1, max_value=99999, label=_("Session keep duration"), + help_text=_("Units: days, Session, record, command will be delete " + "if more than duration, only in database") + ) + TERMINAL_TELNET_REGEX = forms.CharField( + required=False, label=_("Telnet login regex"), + help_text=_("ex: Last\s*login|success|成功") + ) diff --git a/apps/settings/models.py b/apps/settings/models.py index 524fa9349..75b56bb54 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -1,14 +1,11 @@ import json from django.db import models -from django.core.cache import cache from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import ugettext_lazy as _ -from django.conf import settings +from django.core.cache import cache -from common.utils import get_signer - -signer = get_signer() +from common.utils import signer class SettingQuerySet(models.QuerySet): @@ -34,12 +31,28 @@ class Setting(models.Model): comment = models.TextField(verbose_name=_("Comment")) objects = SettingManager() + cache_key_prefix = '_SETTING_' def __str__(self): return self.name - def __getattr__(self, item): - return cache.get(item) + @classmethod + def get(cls, item): + cached = cls.get_from_cache(item) + if cached is not None: + return cached + instances = cls.objects.filter(name=item) + if len(instances) == 1: + s = instances[0] + s.refresh_setting() + return s.cleaned_value + return None + + @classmethod + def get_from_cache(cls, item): + key = cls.cache_key_prefix + item + cached = cache.get(key) + return cached @property def cleaned_value(self): @@ -64,44 +77,6 @@ class Setting(models.Model): except json.JSONDecodeError as e: raise ValueError("Json dump error: {}".format(str(e))) - @classmethod - def save_storage(cls, name, data): - """ - :param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE - :param data: {} - :return: Setting object - """ - obj = cls.objects.filter(name=name).first() - if not obj: - obj = cls() - obj.name = name - obj.encrypted = True - obj.cleaned_value = data - else: - value = obj.cleaned_value - if value is None: - value = {} - value.update(data) - obj.cleaned_value = value - obj.save() - return obj - - @classmethod - def delete_storage(cls, name, storage_name): - """ - :param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE - :param storage_name: "" - :return: bool - """ - obj = cls.objects.filter(name=name).first() - if not obj: - return False - value = obj.cleaned_value - value.pop(storage_name, '') - obj.cleaned_value = value - obj.save() - return True - @classmethod def refresh_all_settings(cls): try: @@ -112,16 +87,8 @@ class Setting(models.Model): pass def refresh_setting(self): - setattr(settings, self.name, self.cleaned_value) - if self.name == "AUTH_LDAP": - if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: - old_setting = settings.AUTHENTICATION_BACKENDS - old_setting.insert(0, settings.AUTH_LDAP_BACKEND) - settings.AUTHENTICATION_BACKENDS = old_setting - elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: - old_setting = settings.AUTHENTICATION_BACKENDS - old_setting.remove(settings.AUTH_LDAP_BACKEND) - settings.AUTHENTICATION_BACKENDS = old_setting + key = self.cache_key_prefix + self.name + cache.set(key, self.cleaned_value, None) class Meta: db_table = "settings_setting" diff --git a/apps/settings/serializers/__init__.py b/apps/settings/serializers/__init__.py new file mode 100644 index 000000000..045763364 --- /dev/null +++ b/apps/settings/serializers/__init__.py @@ -0,0 +1,6 @@ +# coding: utf-8 +# + +from .email import * +from .ldap import * +from .public import * diff --git a/apps/settings/serializers/email.py b/apps/settings/serializers/email.py new file mode 100644 index 000000000..6d033f6aa --- /dev/null +++ b/apps/settings/serializers/email.py @@ -0,0 +1,17 @@ +# coding: utf-8 +# + +from rest_framework import serializers + +__all__ = ['MailTestSerializer'] + + +class MailTestSerializer(serializers.Serializer): + EMAIL_HOST = serializers.CharField(max_length=1024, required=True) + EMAIL_PORT = serializers.IntegerField(default=25) + EMAIL_HOST_USER = serializers.CharField(max_length=1024) + EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True) + EMAIL_FROM = serializers.CharField(required=False, allow_blank=True) + EMAIL_RECIPIENT = serializers.CharField(required=False, allow_blank=True) + EMAIL_USE_SSL = serializers.BooleanField(default=False) + EMAIL_USE_TLS = serializers.BooleanField(default=False) diff --git a/apps/settings/serializers.py b/apps/settings/serializers/ldap.py similarity index 54% rename from apps/settings/serializers.py rename to apps/settings/serializers/ldap.py index 1717634f4..4009c0705 100644 --- a/apps/settings/serializers.py +++ b/apps/settings/serializers/ldap.py @@ -1,15 +1,9 @@ +# coding: utf-8 +# + from rest_framework import serializers - -class MailTestSerializer(serializers.Serializer): - EMAIL_HOST = serializers.CharField(max_length=1024, required=True) - EMAIL_PORT = serializers.IntegerField(default=25) - EMAIL_HOST_USER = serializers.CharField(max_length=1024) - EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True) - EMAIL_FROM = serializers.CharField(required=False, allow_blank=True) - EMAIL_RECIPIENT = serializers.CharField(required=False, allow_blank=True) - EMAIL_USE_SSL = serializers.BooleanField(default=False) - EMAIL_USE_TLS = serializers.BooleanField(default=False) +__all__ = ['LDAPTestSerializer', 'LDAPUserSerializer'] class LDAPTestSerializer(serializers.Serializer): @@ -29,6 +23,3 @@ class LDAPUserSerializer(serializers.Serializer): email = serializers.CharField() existing = serializers.BooleanField(read_only=True) - -class PublicSettingSerializer(serializers.Serializer): - data = serializers.DictField(read_only=True) diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py new file mode 100644 index 000000000..52e39a954 --- /dev/null +++ b/apps/settings/serializers/public.py @@ -0,0 +1,10 @@ +# coding: utf-8 +# + +from rest_framework import serializers + +__all__ = ['PublicSettingSerializer'] + + +class PublicSettingSerializer(serializers.Serializer): + data = serializers.DictField(read_only=True) diff --git a/apps/settings/signals_handler.py b/apps/settings/signals_handler.py index c131cc214..026d60a78 100644 --- a/apps/settings/signals_handler.py +++ b/apps/settings/signals_handler.py @@ -4,9 +4,6 @@ import json from django.dispatch import receiver from django.db.models.signals import post_save, pre_save -from django.conf import LazySettings, empty, global_settings -from django.db.utils import ProgrammingError, OperationalError -from django.core.cache import cache from jumpserver.utils import current_request from common.utils import get_logger, ssh_key_gen @@ -23,56 +20,9 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): @receiver(django_ready) -def monkey_patch_settings(sender, **kwargs): - logger.debug("Monkey patch settings") - cache_key_prefix = '_SETTING_' - custom_need_cache_settings = [ - 'AUTHENTICATION_BACKENDS', 'TERMINAL_HOST_KEY', - ] - custom_no_cache_settings = [ - 'BASE_DIR', 'VERSION', 'AUTH_OPENID', - ] - django_settings = dir(global_settings) - uncached_settings = [i for i in django_settings if i.isupper()] - uncached_settings = [i for i in uncached_settings if not i.startswith('EMAIL')] - uncached_settings = [i for i in uncached_settings if not i.startswith('SESSION_REDIS')] - uncached_settings = [i for i in uncached_settings if i not in custom_need_cache_settings] - uncached_settings.extend(custom_no_cache_settings) - - def monkey_patch_getattr(self, name): - if name not in uncached_settings: - key = cache_key_prefix + name - cached = cache.get(key) - if cached is not None: - return cached - if self._wrapped is empty: - self._setup(name) - val = getattr(self._wrapped, name) - return val - - def monkey_patch_setattr(self, name, value): - key = cache_key_prefix + name - cache.set(key, value, None) - if name == '_wrapped': - self.__dict__.clear() - else: - self.__dict__.pop(name, None) - super(LazySettings, self).__setattr__(name, value) - - def monkey_patch_delattr(self, name): - super(LazySettings, self).__delattr__(name) - self.__dict__.pop(name, None) - key = cache_key_prefix + name - cache.delete(key) - - try: - cache.delete_pattern(cache_key_prefix+'*') - LazySettings.__getattr__ = monkey_patch_getattr - LazySettings.__setattr__ = monkey_patch_setattr - LazySettings.__delattr__ = monkey_patch_delattr - Setting.refresh_all_settings() - except (ProgrammingError, OperationalError): - pass +def on_django_ready_add_db_config(sender, **kwargs): + from django.conf import settings + settings.DYNAMIC.db_setting = Setting @receiver(django_ready) diff --git a/apps/settings/templates/settings/_setting_tabs.html b/apps/settings/templates/settings/_setting_tabs.html new file mode 100644 index 000000000..b012a6669 --- /dev/null +++ b/apps/settings/templates/settings/_setting_tabs.html @@ -0,0 +1,37 @@ +{% load i18n %} + + + diff --git a/apps/settings/templates/settings/basic_setting.html b/apps/settings/templates/settings/basic_setting.html index 4c26e8bb3..ac8cabb8b 100644 --- a/apps/settings/templates/settings/basic_setting.html +++ b/apps/settings/templates/settings/basic_setting.html @@ -10,26 +10,7 @@
    diff --git a/apps/settings/templates/settings/command_storage_create.html b/apps/settings/templates/settings/command_storage_create.html deleted file mode 100644 index 9c60e2d85..000000000 --- a/apps/settings/templates/settings/command_storage_create.html +++ /dev/null @@ -1,175 +0,0 @@ -{#{% extends 'base.html' %}#} -{% extends '_base_create_update.html' %} -{% load static %} -{% load bootstrap3 %} -{% load i18n %} -{% load common_tags %} - -{% block content %} -
    -
    -
    -
    -
    -
    {{ action }}
    - -
    - -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    - - -{# #} - - - - -
    -
    -
    - - {% trans 'Submit' %} -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/settings/templates/settings/email_content_setting.html b/apps/settings/templates/settings/email_content_setting.html index 16cac426e..c2c5b2720 100644 --- a/apps/settings/templates/settings/email_content_setting.html +++ b/apps/settings/templates/settings/email_content_setting.html @@ -10,53 +10,33 @@
    -
    -
    - {% if form.non_field_errors %} -
    - {{ form.non_field_errors }} -
    - {% endif %} - {% csrf_token %} +
    + + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} -

    {% trans "Create User setting" %}

    - {% 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" %} -
    +

    {% trans "Create User setting" %}

    + {% 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" %} +
    -
    -
    - - -
    +
    +
    + +
    -
    +
    diff --git a/apps/settings/templates/settings/email_setting.html b/apps/settings/templates/settings/email_setting.html index 40ce9f4cc..d62a921cd 100644 --- a/apps/settings/templates/settings/email_setting.html +++ b/apps/settings/templates/settings/email_setting.html @@ -10,26 +10,7 @@
    diff --git a/apps/settings/templates/settings/ldap_setting.html b/apps/settings/templates/settings/ldap_setting.html index 5da66405d..943a69322 100644 --- a/apps/settings/templates/settings/ldap_setting.html +++ b/apps/settings/templates/settings/ldap_setting.html @@ -10,26 +10,7 @@
    diff --git a/apps/settings/templates/settings/replay_storage_create.html b/apps/settings/templates/settings/replay_storage_create.html deleted file mode 100644 index 6521a730a..000000000 --- a/apps/settings/templates/settings/replay_storage_create.html +++ /dev/null @@ -1,277 +0,0 @@ -{#{% extends 'base.html' %}#} -{% extends '_base_create_update.html' %} -{% load static %} -{% load bootstrap3 %} -{% load i18n %} -{% load common_tags %} - -{% block content %} -
    -
    -
    -
    -
    -
    {{ action }}
    - -
    - -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - - {% trans 'Submit' %} -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/settings/templates/settings/security_setting.html b/apps/settings/templates/settings/security_setting.html index 48206676d..663632a72 100644 --- a/apps/settings/templates/settings/security_setting.html +++ b/apps/settings/templates/settings/security_setting.html @@ -10,26 +10,7 @@
    @@ -44,7 +25,7 @@

    {% trans "Security setting" %}

    {% for field in form %} - {% if forloop.counter == 6 %} + {% if forloop.counter == 8 %}

    {% trans "Password check rule" %}

    {% endif %} diff --git a/apps/settings/templates/settings/terminal_setting.html b/apps/settings/templates/settings/terminal_setting.html index 3a9a4973d..a0b35aabb 100644 --- a/apps/settings/templates/settings/terminal_setting.html +++ b/apps/settings/templates/settings/terminal_setting.html @@ -3,6 +3,11 @@ {% load bootstrap3 %} {% load i18n %} {% load common_tags %} +{% block help_message %} + {% trans "Command and Replay storage configuration migrated to" %} + {% trans "Sessions -> Terminal -> Storage configuration" %} + {% trans 'Here' %} +{% endblock %} {% block content %}
    @@ -10,30 +15,7 @@
    @@ -65,7 +47,7 @@
    {% endif %} {% endfor %} - +
    @@ -73,53 +55,6 @@ type="submit">{% trans 'Submit' %}
    - -
    - -

    {% trans "Command storage" %}

    - - - - - - - - - - {% for name, setting in command_storage.items %} - - - - - - {% endfor %} - -
    {% trans 'Name' %}{% trans 'Type' %}{% trans 'Action' %}
    {{ name }}{{ setting.TYPE }}{% trans 'Delete' %}
    - {% trans 'Add' %} - -
    -

    {% trans "Replay storage" %}

    - - - - - - - - - - {% for name, setting in replay_storage.items %} - - - - - - {% endfor %} - -
    {% trans 'Name' %}{% trans 'Type' %}{% trans 'Action' %}
    {{ name }}{{ setting.TYPE }}{% trans 'Delete' %}
    - {% trans 'Add' %} - -
    @@ -131,60 +66,7 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 544de3941..abee1c0d0 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -12,9 +12,6 @@ urlpatterns = [ path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), - path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'), - path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'), - path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'), - path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'), + path('public/', api.PublicSettingApi.as_view(), name='public-setting'), ] diff --git a/apps/settings/urls/view_urls.py b/apps/settings/urls/view_urls.py index eac18a432..6a1c5baaf 100644 --- a/apps/settings/urls/view_urls.py +++ b/apps/settings/urls/view_urls.py @@ -12,7 +12,5 @@ urlpatterns = [ url(r'^email-content/$', views.EmailContentSettingView.as_view(), name='email-content-setting'), url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-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/command-storage/create$', views.CommandStorageCreateView.as_view(), name='command-storage-create'), url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'), ] diff --git a/apps/settings/views.py b/apps/settings/views.py index 2442f074e..6ebbeef97 100644 --- a/apps/settings/views.py +++ b/apps/settings/views.py @@ -4,7 +4,6 @@ from django.contrib import messages from django.utils.translation import ugettext as _ from common.permissions import PermissionsMixin, IsSuperUser -from common import utils from .utils import LDAPSyncUtil from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ TerminalSettingForm, SecuritySettingForm, EmailContentSettingForm @@ -98,8 +97,9 @@ class TerminalSettingView(PermissionsMixin, TemplateView): permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): - command_storage = utils.get_command_storage_setting() - replay_storage = utils.get_replay_storage_setting() + from terminal.models import CommandStorage, ReplayStorage + command_storage = CommandStorage.objects.all() + replay_storage = ReplayStorage.objects.all() context = { 'app': _('Settings'), @@ -124,32 +124,6 @@ class TerminalSettingView(PermissionsMixin, TemplateView): return render(request, self.template_name, context) -class ReplayStorageCreateView(PermissionsMixin, TemplateView): - template_name = 'settings/replay_storage_create.html' - permission_classes = [IsSuperUser] - - def get_context_data(self, **kwargs): - context = { - 'app': _('Settings'), - 'action': _('Create replay storage') - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class CommandStorageCreateView(PermissionsMixin, TemplateView): - template_name = 'settings/command_storage_create.html' - permission_classes = [IsSuperUser] - - def get_context_data(self, **kwargs): - context = { - 'app': _('Settings'), - 'action': _('Create command storage') - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - class SecuritySettingView(PermissionsMixin, TemplateView): form_class = SecuritySettingForm template_name = "settings/security_setting.html" diff --git a/apps/static/css/plugins/ladda/ladda-themeless.min.css b/apps/static/css/plugins/ladda/ladda-themeless.min.css new file mode 100755 index 000000000..6dee68811 --- /dev/null +++ b/apps/static/css/plugins/ladda/ladda-themeless.min.css @@ -0,0 +1,7 @@ +/*! + * Ladda + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2015 Hakim El Hattab, http://hakim.se + */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1} diff --git a/apps/static/css/plugins/ladda/ladda.min.css b/apps/static/css/plugins/ladda/ladda.min.css new file mode 100755 index 000000000..7a42a10f0 --- /dev/null +++ b/apps/static/css/plugins/ladda/ladda.min.css @@ -0,0 +1,9 @@ +/*! + * Ladda including the default theme. + *//*! + * Ladda + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2015 Hakim El Hattab, http://hakim.se + */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1}.ladda-button{background:#666;border:0;padding:14px 18px;font-size:18px;cursor:pointer;color:#fff;border-radius:2px;border:1px solid transparent;-webkit-appearance:none;-webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent}.ladda-button:hover{border-color:rgba(0,0,0,0.07);background-color:#888}.ladda-button[data-color=green]{background:#2aca76}.ladda-button[data-color=green]:hover{background-color:#38d683}.ladda-button[data-color=blue]{background:#53b5e6}.ladda-button[data-color=blue]:hover{background-color:#69bfe9}.ladda-button[data-color=red]{background:#ea8557}.ladda-button[data-color=red]:hover{background-color:#ed956e}.ladda-button[data-color=purple]{background:#9973C2}.ladda-button[data-color=purple]:hover{background-color:#a685ca}.ladda-button[data-color=mint]{background:#16a085}.ladda-button[data-color=mint]:hover{background-color:#19b698}.ladda-button[disabled],.ladda-button[data-loading]{border-color:rgba(0,0,0,0.07)}.ladda-button[disabled],.ladda-button[disabled]:hover,.ladda-button[data-loading],.ladda-button[data-loading]:hover{cursor:default;background-color:#999}.ladda-button[data-size=xs]{padding:4px 8px}.ladda-button[data-size=xs] .ladda-label{font-size:0.7em}.ladda-button[data-size=s]{padding:6px 10px}.ladda-button[data-size=s] .ladda-label{font-size:0.9em}.ladda-button[data-size=l] .ladda-label{font-size:1.2em}.ladda-button[data-size=xl] .ladda-label{font-size:1.5em} diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 773023f16..d334d6c64 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -158,7 +158,7 @@ function activeNav(prefix) { } else { $("#" + app).addClass('active'); $('#' + app + ' #' + resource).addClass('active'); - $('#' + app + ' #' + resource.replaceAll('-', '_')).addClass('active'); + $('#' + app + ' #' + resource.replace(/-/g, '_')).addClass('active'); } } @@ -177,7 +177,7 @@ function formSubmit(props) { */ props = props || {}; var data = props.data || props.form.serializeObject(); - var redirect_to = props.redirect_to; + var redirectTo = props.redirect_to || props.redirectTo; $.ajax({ url: props.url, type: props.method || 'POST', @@ -185,12 +185,8 @@ function formSubmit(props) { contentType: props.content_type || "application/json; charset=utf-8", dataType: props.data_type || "json" }).done(function (data, textState, jqXHR) { - if (redirect_to) { - if (props.message) { - var messages = "ed65330a45559c87345a0eb6ac7812d18d0d8976$[[\"__json_message\"\0540\05425\054\"asdfasdf \\u521b\\u5efa\\u6210\\u529f\"]]" - setCookie("messages", messages) - } - location.href = redirect_to; + if (redirectTo) { + location.href = redirectTo; } else if (typeof props.success === 'function') { return props.success(data, textState, jqXHR); } @@ -254,7 +250,6 @@ function formSubmit(props) { } $('.has-error').get(0).scrollIntoView(); } - }) } @@ -316,7 +311,7 @@ function requestApi(props) { } // Sweet Alert for Delete -function objectDelete(obj, name, url, redirectTo) { +function objectDelete(obj, name, url, redirectTo, title, success_message) { function doDelete() { var body = {}; var success = function () { @@ -335,14 +330,14 @@ function objectDelete(obj, name, url, redirectTo) { url: url, body: JSON.stringify(body), method: 'DELETE', - success_message: gettext("Delete the success"), + success_message: success_message || gettext("Delete the success"), success: success, error: fail }); } swal({ - title: gettext('Are you sure about deleting it?'), + title: title || gettext('Are you sure about deleting it?'), text: " [" + name + "] ", type: "warning", showCancelButton: true, @@ -413,7 +408,7 @@ $.fn.serializeObject = function () { }; function makeLabel(data) { - return "" + data[1] + "
    " + return " " + data[1] + "
    " } function parseTableFilter(value) { @@ -600,6 +595,7 @@ jumpserver.initServerSideDataTable = function (options) { // op_html: 'div.btn-group?', // paging: true, // paging_numbers_length: 5; + // hideDefaultDefs: false; // } var pagingNumbersLength = 5; if (options.paging_numbers_length){ @@ -613,7 +609,8 @@ jumpserver.initServerSideDataTable = function (options) { orderable: false, width: "20px", createdCell: function (td, cellData) { - $(td).html(''.replace('99991937', cellData)); + var data = ''.replace('Id', cellData); + $(td).html(data); } }, { @@ -622,6 +619,9 @@ jumpserver.initServerSideDataTable = function (options) { render: $.fn.dataTable.render.text() } ]; + if (options.hideDefaultDefs) { + columnDefs = []; + } var select_style = options.select_style || 'multi'; columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs; var select = { @@ -635,7 +635,7 @@ jumpserver.initServerSideDataTable = function (options) { pageLength: options.pageLength || 15, // dom: options.dom || '<"#uc.pull-left">fltr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>', // dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"<"table-filter"f>><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>', - dom: dom, + dom: options.dom || dom, order: options.order || [], buttons: [], columnDefs: columnDefs, @@ -713,8 +713,14 @@ jumpserver.initServerSideDataTable = function (options) { var rows = table.rows(indexes).data(); $.each(rows, function (id, row) { if (row.id && $.inArray(row.id, table.selected) === -1) { - table.selected.push(row.id); - table.selected_rows.push(row); + if (select.style === 'multi'){ + table.selected.push(row.id); + table.selected_rows.push(row); + } + else{ + table.selected = [row.id]; + table.selected_rows = [row]; + } } }) } @@ -1027,6 +1033,62 @@ function rootNodeAddDom(ztree, callback) { }) } +function APIExportCSV(props) { + /* + { + listUrl: + objectsId: + template: + table: + params: + } + */ + var _listUrl = props.listUrl; + var _objectsId = props.objectsId; + var _template = props.template; + var _table = props.table; + var _params = props.params || {}; + + var tableParams = _table.ajax.params(); + var exportUrl = setUrlParam(_listUrl, 'format', 'csv'); + if (_template) { + exportUrl = setUrlParam(exportUrl, 'template', _template) + } + for (var k in tableParams) { + if (datatableInternalParams.includes(k)) { + continue + } + if (!tableParams[k]) { + continue + } + exportUrl = setUrlParam(exportUrl, k, tableParams[k]) + } + for (var k in _params) { + exportUrl = setUrlParam(exportUrl, k, tableParams[k]) + } + + if (!_objectsId) { + console.log(exportUrl); + window.open(exportUrl); + return + } + + requestApi({ + url: '/api/v1/common/resources/cache/', + data: JSON.stringify({resources: _objectsId}), + method: "POST", + flash_message: false, + success: function (data) { + exportUrl = setUrlParam(exportUrl, 'spm', data.spm); + console.log(exportUrl); + window.open(exportUrl); + }, + failed: function () { + toastr.error(gettext('Export failed')); + } + }); +} + function APIExportData(props) { props = props || {}; $.ajax({ @@ -1076,6 +1138,7 @@ function APIImportData(props) { }, error: function (error) { var data = error.responseJSON; + console.log(data); if (data instanceof Array) { var html = ''; var li = ''; @@ -1136,8 +1199,8 @@ function objectAttrsIsBool(obj, attrs) { attrs.forEach(function (attr) { if (!obj[attr]) { obj[attr] = false - } else if (['on', '1'].includes(obj[attr])) { - obj[attr] = true + } else { + obj[attr] = ['on', '1', 'true', 'True'].includes(obj[attr]); } }) } @@ -1241,10 +1304,28 @@ function readFile(ref) { return ref } -function nodesSelect2Init(selector, url) { - if (!url) { - url = '/api/v1/assets/nodes/' + + +function select2AjaxInit(option) { + /* + { + selector: + url: , + disabledData: , + displayFormat, + idFormat, } + */ + var selector = option.selector; + var url = option.url; + var disabledData = option.disabledData; + var displayFormat = option.displayFormat || function (data) { + return data.name; + }; + var idFormat = option.idFormat || function (data) { + return data.id; + }; + return $(selector).select2({ closeOnSelect: false, ajax: { @@ -1260,43 +1341,53 @@ function nodesSelect2Init(selector, url) { }, processResults: function (data) { var results = $.map(data.results, function (v, i) { - return {id: v.id, text: v.full_value} + var display = displayFormat(v); + var id = idFormat(v); + var d = {id: id, text: display}; + if (disabledData && disabledData.indexOf(v.id) !== -1) { + d.disabled = true; + } + return d; }); var more = !!data.next; return {results: results, pagination: {"more": more}} } }, }) + } -function usersSelect2Init(selector, url) { +function usersSelect2Init(selector, url, disabledData) { if (!url) { url = '/api/v1/users/users/' } - return $(selector).select2({ - closeOnSelect: false, - ajax: { - url: url, - data: function (params) { - var page = params.page || 1; - var query = { - search: params.term, - offset: (page - 1) * 10, - limit: 10 - }; - return query - }, - processResults: function (data) { - var results = $.map(data.results, function (v, i) { - var display = v.name + '(' + v.username +')'; - return {id: v.id, text: display} - }); - var more = !!data.next; - return {results: results, pagination: {"more": more}} - } - }, - }) + function displayFormat(v) { + return v.name + '(' + v.username +')'; + } + var option = { + url: url, + selector: selector, + disabledData: disabledData, + displayFormat: displayFormat + }; + return select2AjaxInit(option) +} + +function nodesSelect2Init(selector, url, disabledData) { + if (!url) { + url = '/api/v1/assets/nodes/' + } + function displayFormat(v) { + return v.full_value; + } + var option = { + url: url, + selector: selector, + disabledData: disabledData, + displayFormat: displayFormat + }; + return select2AjaxInit(option) } function showCeleryTaskLog(taskId) { @@ -1324,7 +1415,7 @@ function initDateRangePicker(selector, options) { timePicker24Hour: true, autoApply: true, }; - var userLang = navigator.language || navigator.userLanguage;; + var userLang = navigator.language || navigator.userLanguage; if (userLang.indexOf('zh') !== -1) { defaultOption.locale = zhLocale; } diff --git a/apps/static/js/plugins/ladda/ladda.jquery.min.js b/apps/static/js/plugins/ladda/ladda.jquery.min.js new file mode 100755 index 000000000..74fb3ae03 --- /dev/null +++ b/apps/static/js/plugins/ladda/ladda.jquery.min.js @@ -0,0 +1,8 @@ +/*! + * Ladda for jQuery + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2015 Hakim El Hattab, http://hakim.se + */ +!function(a,b){if(void 0===b)return console.error("jQuery required for Ladda.jQuery");var c=[];b=b.extend(b,{ladda:function(b){"stopAll"===b&&a.stopAll()}}),b.fn=b.extend(b.fn,{ladda:function(d){var e=c.slice.call(arguments,1);return"bind"===d?(e.unshift(b(this).selector),a.bind.apply(a,e)):b(this).each(function(){var c,f=b(this);void 0===d?f.data("ladda",a.create(this)):(c=f.data("ladda"),c[d].apply(c,e))}),this}})}(this.Ladda,this.jQuery); \ No newline at end of file diff --git a/apps/static/js/plugins/ladda/ladda.min.js b/apps/static/js/plugins/ladda/ladda.min.js new file mode 100755 index 000000000..f7f81ec74 --- /dev/null +++ b/apps/static/js/plugins/ladda/ladda.min.js @@ -0,0 +1,8 @@ +/*! + * Ladda 1.0.0 (2016-03-08, 09:31) + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2016 Hakim El Hattab, http://hakim.se + */ +!function(a,b){"object"==typeof exports?module.exports=b(require("spin.js")):"function"==typeof define&&define.amd?define(["spin"],b):a.Ladda=b(a.Spinner)}(this,function(a){"use strict";function b(a){if("undefined"==typeof a)return void console.warn("Ladda button target must be defined.");if(/ladda-button/i.test(a.className)||(a.className+=" ladda-button"),a.hasAttribute("data-style")||a.setAttribute("data-style","expand-right"),!a.querySelector(".ladda-label")){var b=document.createElement("span");b.className="ladda-label",i(a,b)}var c,d=a.querySelector(".ladda-spinner");d||(d=document.createElement("span"),d.className="ladda-spinner"),a.appendChild(d);var e,f={start:function(){return c||(c=g(a)),a.setAttribute("disabled",""),a.setAttribute("data-loading",""),clearTimeout(e),c.spin(d),this.setProgress(0),this},startAfter:function(a){return clearTimeout(e),e=setTimeout(function(){f.start()},a),this},stop:function(){return a.removeAttribute("disabled"),a.removeAttribute("data-loading"),clearTimeout(e),c&&(e=setTimeout(function(){c.stop()},1e3)),this},toggle:function(){return this.isLoading()?this.stop():this.start(),this},setProgress:function(b){b=Math.max(Math.min(b,1),0);var c=a.querySelector(".ladda-progress");0===b&&c&&c.parentNode?c.parentNode.removeChild(c):(c||(c=document.createElement("div"),c.className="ladda-progress",a.appendChild(c)),c.style.width=(b||0)*a.offsetWidth+"px")},enable:function(){return this.stop(),this},disable:function(){return this.stop(),a.setAttribute("disabled",""),this},isLoading:function(){return a.hasAttribute("data-loading")},remove:function(){clearTimeout(e),a.removeAttribute("disabled",""),a.removeAttribute("data-loading",""),c&&(c.stop(),c=null);for(var b=0,d=j.length;d>b;b++)if(f===j[b]){j.splice(b,1);break}}};return j.push(f),f}function c(a,b){for(;a.parentNode&&a.tagName!==b;)a=a.parentNode;return b===a.tagName?a:void 0}function d(a){for(var b=["input","textarea","select"],c=[],d=0;dg;g++)!function(){var a=f[g];if("function"==typeof a.addEventListener){var h=b(a),i=-1;a.addEventListener("click",function(b){var f=!0,g=c(a,"FORM");if("undefined"!=typeof g)if("function"==typeof g.checkValidity)f=g.checkValidity();else for(var j=d(g),k=0;ka;a++)j[a].stop()}function g(b){var c,d,e=b.offsetHeight;0===e&&(e=parseFloat(window.getComputedStyle(b).height)),e>32&&(e*=.8),b.hasAttribute("data-spinner-size")&&(e=parseInt(b.getAttribute("data-spinner-size"),10)),b.hasAttribute("data-spinner-color")&&(c=b.getAttribute("data-spinner-color")),b.hasAttribute("data-spinner-lines")&&(d=parseInt(b.getAttribute("data-spinner-lines"),10));var f=.2*e,g=.6*f,h=7>f?2:3;return new a({color:c||"#fff",lines:d||12,radius:f,length:g,width:h,zIndex:"auto",top:"auto",left:"auto",className:""})}function h(a){for(var b=[],c=0;cb;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return l[e]||(m.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",m.cssRules.length),l[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d',c)}m.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k +
    +
    + {% include 'assets/_node_tree.html' %} +
    +
    +
    +
    + +
    +
    +
    + {% block table_container %} + + + + {% block table_head %} {% endblock %} + + + + {% block table_body %} {% endblock %} + +
    + {% endblock %} +
    +
    +
    +
    + +{% endblock %} diff --git a/apps/templates/_base_create_update.html b/apps/templates/_base_create_update.html index be3813804..d206d40b2 100644 --- a/apps/templates/_base_create_update.html +++ b/apps/templates/_base_create_update.html @@ -3,8 +3,6 @@ {% load static %} {% load bootstrap3 %} {% block custom_head_css_js %} - - {% block custom_head_css_js_create %} {% endblock %} {% endblock %} diff --git a/apps/templates/_base_list.html b/apps/templates/_base_list.html index c5314af4e..9759081bd 100644 --- a/apps/templates/_base_list.html +++ b/apps/templates/_base_list.html @@ -1,10 +1,6 @@ {% extends 'base.html' %} {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    diff --git a/apps/templates/_base_only_content.html b/apps/templates/_base_only_content.html new file mode 100644 index 000000000..1e6a16c84 --- /dev/null +++ b/apps/templates/_base_only_content.html @@ -0,0 +1,47 @@ +{% load static %} +{% load i18n %} + + + + + + + + {% block html_title %}{% endblock %} + + {% include '_head_css_js.html' %} + + + + + {% block custom_head_css_js %} {% endblock %} + + + +
    +
    +
    +
    + +

    {% block title %}{% endblock %}

    +

    + {% block content %} {% endblock %} +
    +
    +
    +
    +
    +
    + {% include '_copyright.html' %} +
    +
    +
    + +{% block custom_foot_js %} {% endblock %} + diff --git a/apps/templates/_csv_import_export.html b/apps/templates/_csv_import_export.html new file mode 100644 index 000000000..d22c947b4 --- /dev/null +++ b/apps/templates/_csv_import_export.html @@ -0,0 +1,62 @@ +{% load i18n %} + +{% include '_csv_import_modal.html' %} +{% include '_csv_update_modal.html' %} + + diff --git a/apps/templates/_csv_import_modal.html b/apps/templates/_csv_import_modal.html new file mode 100644 index 000000000..4925793d0 --- /dev/null +++ b/apps/templates/_csv_import_modal.html @@ -0,0 +1,52 @@ +{% extends '_modal.html' %} +{% load i18n %} + +{% block modal_id %}csv_import_modal{% endblock %} +{% block modal_title%}csv {% trans 'Import' %}{% endblock %} +{% block modal_confirm_id %}btn_csv_import_confirm{% endblock %} + +{% block modal_body %} +
    + {% csrf_token %} +
    + + {% trans 'Download the import template' %} +
    + +
    + + +
    +
    + +
    +

    +

    +

    +

    +
    + + +{% endblock %} + + diff --git a/apps/templates/_csv_update_modal.html b/apps/templates/_csv_update_modal.html new file mode 100644 index 000000000..c4c31abda --- /dev/null +++ b/apps/templates/_csv_update_modal.html @@ -0,0 +1,54 @@ +{% extends '_modal.html' %} +{% load i18n %} + +{% block modal_id %}csv_update_modal{% endblock %} +{% block modal_confirm_id %}btn_csv_update_confirm{% endblock %} +{% block modal_title%}csv {% trans 'Update' %}{% endblock %} + +{% block modal_body %} +
    + {% csrf_token %} +
    + + {% trans 'Download the update template' %} +
    + +
    + + +
    +
    + +
    +

    +

    +

    +

    +
    + + +{% endblock %} diff --git a/apps/templates/_foot_js.html b/apps/templates/_foot_js.html index 509b23ad3..22161696f 100644 --- a/apps/templates/_foot_js.html +++ b/apps/templates/_foot_js.html @@ -7,7 +7,9 @@ - + + + diff --git a/apps/templates/_head_css_js.html b/apps/templates/_head_css_js.html index 45694e6fc..b4fbcd47b 100644 --- a/apps/templates/_head_css_js.html +++ b/apps/templates/_head_css_js.html @@ -13,3 +13,5 @@ + + diff --git a/apps/templates/_import_modal.html b/apps/templates/_import_modal.html deleted file mode 100644 index 9211bdcb9..000000000 --- a/apps/templates/_import_modal.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends '_modal.html' %} -{% load i18n %} - -{% block modal_id %}import_modal{% endblock %} - -{% block modal_confirm_id %}btn_import_confirm{% endblock %} - -{% block modal_body %} -
    - {% csrf_token %} -
    - - {% trans 'Download the import template' %} -
    - -
    - - -
    -
    - -
    -

    -

    -

    -

    -
    -{% endblock %} diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 03f9d83cb..5ad4a2618 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -45,6 +45,9 @@
  • {% trans 'System user' %}
  • {% trans 'Labels' %}
  • {% trans 'Command filters' %}
  • + {% if request.user.is_superuser %} +
  • {% trans 'Platform list' %}
  • + {% endif %} {% endif %} @@ -58,6 +61,7 @@ {% endif %} @@ -75,6 +79,9 @@
  • {% trans 'RemoteApp' %}
  • +
  • + {% trans 'DatabaseApp' %} +
  • {% endif %} @@ -113,7 +120,7 @@ {% endif %} {% if SECURITY_COMMAND_EXECUTION %}
  • - + {% trans 'Command execution' %}
  • @@ -41,4 +46,4 @@ {% trans 'File manager' %} - \ No newline at end of file + diff --git a/apps/templates/_update_modal.html b/apps/templates/_update_modal.html deleted file mode 100644 index db2b14110..000000000 --- a/apps/templates/_update_modal.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends '_modal.html' %} -{% load i18n %} - -{% block modal_id %}update_modal{% endblock %} - -{% block modal_confirm_id %}btn_update_confirm{% endblock %} - -{% block modal_body %} -
    - {% csrf_token %} -
    - - {% trans 'Download the update template' %} -
    - -
    - - -
    -
    - -
    -

    -

    -

    -

    -
    -{% endblock %} diff --git a/apps/templates/_without_nav_base.html b/apps/templates/_without_nav_base.html new file mode 100644 index 000000000..98bcb6189 --- /dev/null +++ b/apps/templates/_without_nav_base.html @@ -0,0 +1,44 @@ +{% load static %} +{% load i18n %} + + + + + + {{ JMS_TITLE }} + +{# #} + + + + + + + +
    + + +
    + + {% block body %} + {% endblock %} + +
    +
    + {% include '_copyright.html' %} +
    +
    + + + diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html index 304a8bd0a..84ef58cb5 100644 --- a/apps/templates/flash_message_standalone.html +++ b/apps/templates/flash_message_standalone.html @@ -1,83 +1,64 @@ -{% load i18n %} +{% extends '_base_only_content.html' %} {% load static %} - - +{% load i18n %} +{% block html_title %} {{ title }} {% endblock %} +{% block title %} {{ title }}{% endblock %} - - - +{% block custom_head_css_js %} + +{% endblock %} - {{ title }} - - {% include '_head_css_js.html' %} - - - - - - - -
    -
    -
    -
    -
    - -

    - {{ JMS_TITLE }} -

    -
    - {% if errors %} -

    -

    - {{ errors }} -
    -

    - {% endif %} - - {% if messages %} -

    -

    - {{ messages|safe }} -
    -

    - {% endif %} - -
    +{% block content %} +
    + {% if errors %} +

    +

    + {{ errors }}
    -
    -
    +

    + {% endif %} + + {% if messages %} +

    +

    + {{ messages|safe }} +
    +

    + {% endif %}
    -
    - {% include '_copyright.html' %} +
    - +{% endblock %} + +{% block custom_foot_js %} - +{% endblock %} + diff --git a/apps/templates/index.html b/apps/templates/index.html index c97b6e849..76b6ddddd 100644 --- a/apps/templates/index.html +++ b/apps/templates/index.html @@ -477,4 +477,4 @@ $(document).ready(function(){ ); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/apps/terminal/api/__init__.py b/apps/terminal/api/__init__.py index d33905b78..640dacb6a 100644 --- a/apps/terminal/api/__init__.py +++ b/apps/terminal/api/__init__.py @@ -4,3 +4,4 @@ from .terminal import * from .session import * from .command import * from .task import * +from .storage import * diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index 264502959..7607d46de 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -12,10 +12,10 @@ import jms_storage from common.utils import is_uuid, get_logger from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor -from common.filters import DatetimeRangeFilter +from common.drf.filters import DatetimeRangeFilter from orgs.mixins.api import OrgBulkModelViewSet from ..hands import SystemUser -from ..models import Session +from ..models import Session, ReplayStorage from .. import serializers @@ -105,9 +105,12 @@ class SessionReplayViewSet(viewsets.ViewSet): data['src'] = url return Response(data) - # 去定义的外部storage查找 - configs = settings.TERMINAL_REPLAY_STORAGE - configs = {k: v for k, v in configs.items() if v['TYPE'] != 'server'} + replay_storages = ReplayStorage.objects.all() + configs = { + storage.name: storage.config + for storage in replay_storages + if not storage.in_defaults() + } if not configs: return HttpResponseNotFound() diff --git a/apps/terminal/api/storage.py b/apps/terminal/api/storage.py new file mode 100644 index 000000000..ec85b95f7 --- /dev/null +++ b/apps/terminal/api/storage.py @@ -0,0 +1,74 @@ +# coding: utf-8 +# + +from rest_framework import viewsets, generics, status +from rest_framework.response import Response +from django.utils.translation import ugettext_lazy as _ + +from common.permissions import IsSuperUser +from ..models import CommandStorage, ReplayStorage +from ..serializers import CommandStorageSerializer, ReplayStorageSerializer + + +__all__ = [ + 'CommandStorageViewSet', 'CommandStorageTestConnectiveApi', + 'ReplayStorageViewSet', 'ReplayStorageTestConnectiveApi' +] + + +class BaseStorageViewSetMixin: + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if not instance.can_delete(): + data = {'msg': _('Deleting the default storage is not allowed')} + return Response(data=data, status=status.HTTP_400_BAD_REQUEST) + return super().destroy(request, *args, **kwargs) + + +class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): + filter_fields = ('name', 'type',) + search_fields = filter_fields + queryset = CommandStorage.objects.all() + serializer_class = CommandStorageSerializer + permission_classes = (IsSuperUser,) + + +class ReplayStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): + filter_fields = ('name', 'type',) + search_fields = filter_fields + queryset = ReplayStorage.objects.all() + serializer_class = ReplayStorageSerializer + permission_classes = (IsSuperUser,) + + +class BaseStorageTestConnectiveMixin: + permission_classes = (IsSuperUser,) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + try: + is_valid = instance.is_valid() + except Exception as e: + is_valid = False + msg = _("Test failure: {}".format(str(e))) + else: + if is_valid: + msg = _("Test successful") + else: + msg = _("Test failure: Account invalid") + data = { + 'is_valid': is_valid, + 'msg': msg + } + return Response(data) + + +class CommandStorageTestConnectiveApi(BaseStorageTestConnectiveMixin, + generics.RetrieveAPIView): + queryset = CommandStorage.objects.all() + + +class ReplayStorageTestConnectiveApi(BaseStorageTestConnectiveMixin, + generics.RetrieveAPIView): + queryset = ReplayStorage.objects.all() diff --git a/apps/terminal/backends/__init__.py b/apps/terminal/backends/__init__.py index 1aec6b3be..b2a9c557c 100644 --- a/apps/terminal/backends/__init__.py +++ b/apps/terminal/backends/__init__.py @@ -1,8 +1,8 @@ from importlib import import_module from django.conf import settings from .command.serializers import SessionCommandSerializer +from ..const import COMMAND_STORAGE_TYPE_SERVER -from common import utils TYPE_ENGINE_MAPPING = { 'elasticsearch': 'terminal.backends.command.es', @@ -18,19 +18,18 @@ def get_command_storage(): def get_terminal_command_storages(): + from ..models import CommandStorage storage_list = {} - command_storage = utils.get_command_storage_setting() - - for name, params in command_storage.items(): - tp = params['TYPE'] - if tp == 'server': + for s in CommandStorage.objects.all(): + tp = s.type + if tp == COMMAND_STORAGE_TYPE_SERVER: storage = get_command_storage() else: if not TYPE_ENGINE_MAPPING.get(tp): continue engine_class = import_module(TYPE_ENGINE_MAPPING[tp]) - storage = engine_class.CommandStore(params) - storage_list[name] = storage + storage = engine_class.CommandStore(s.config) + storage_list[s.name] = storage return storage_list diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 2d74e00c1..b412c1624 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -5,3 +5,105 @@ ASSETS_CACHE_KEY = "terminal__session__assets" USERS_CACHE_KEY = "terminal__session__users" SYSTEM_USER_CACHE_KEY = "terminal__session__system_users" + +# Replay Storage + +REPLAY_STORAGE_TYPE_NULL = 'null' +REPLAY_STORAGE_TYPE_SERVER = 'server' +REPLAY_STORAGE_TYPE_S3 = 's3' +REPLAY_STORAGE_TYPE_CEPH = 'ceph' +REPLAY_STORAGE_TYPE_SWIFT = 'swift' +REPLAY_STORAGE_TYPE_OSS = 'oss' +REPLAY_STORAGE_TYPE_AZURE = 'azure' + +REPLAY_STORAGE_TYPE_EMPTY_FIELDS = [] +REPLAY_STORAGE_TYPE_S3_FIELDS = [ + {'name': 'BUCKET'}, + {'name': 'ACCESS_KEY', 'write_only': True}, + {'name': 'SECRET_KEY', 'write_only': True}, + {'name': 'ENDPOINT'} +] +REPLAY_STORAGE_TYPE_CEPH_FIELDS = [ + {'name': 'BUCKET'}, + {'name': 'ACCESS_KEY', 'write_only': True}, + {'name': 'SECRET_KEY', 'write_only': True}, + {'name': 'ENDPOINT'} +] +REPLAY_STORAGE_TYPE_SWIFT_FIELDS = [ + {'name': 'BUCKET'}, + {'name': 'ACCESS_KEY', 'write_only': True}, + {'name': 'SECRET_KEY', 'write_only': True}, + {'name': 'REGION'}, + {'name': 'ENDPOINT'}, +] +REPLAY_STORAGE_TYPE_OSS_FIELDS = [ + {'name': 'BUCKET'}, + {'name': 'ACCESS_KEY', 'write_only': True}, + {'name': 'SECRET_KEY', 'write_only': True}, + {'name': 'ENDPOINT'} +] +REPLAY_STORAGE_TYPE_AZURE_FIELDS = [ + {'name': 'CONTAINER_NAME'}, + {'name': 'ACCOUNT_NAME'}, + {'name': 'ACCOUNT_KEY', 'write_only': True}, + {'name': 'ENDPOINT_SUFFIX'} +] + +REPLAY_STORAGE_TYPE_FIELDS_MAP = { + REPLAY_STORAGE_TYPE_NULL: REPLAY_STORAGE_TYPE_EMPTY_FIELDS, + REPLAY_STORAGE_TYPE_SERVER: REPLAY_STORAGE_TYPE_EMPTY_FIELDS, + REPLAY_STORAGE_TYPE_S3: REPLAY_STORAGE_TYPE_S3_FIELDS, + REPLAY_STORAGE_TYPE_CEPH: REPLAY_STORAGE_TYPE_CEPH_FIELDS, + REPLAY_STORAGE_TYPE_SWIFT: REPLAY_STORAGE_TYPE_SWIFT_FIELDS, + REPLAY_STORAGE_TYPE_OSS: REPLAY_STORAGE_TYPE_OSS_FIELDS, + REPLAY_STORAGE_TYPE_AZURE: REPLAY_STORAGE_TYPE_AZURE_FIELDS +} + +REPLAY_STORAGE_TYPE_CHOICES_DEFAULT = [ + (REPLAY_STORAGE_TYPE_NULL, 'Null'), + (REPLAY_STORAGE_TYPE_SERVER, 'Server'), +] + +REPLAY_STORAGE_TYPE_CHOICES_EXTENDS = [ + (REPLAY_STORAGE_TYPE_S3, 'S3'), + (REPLAY_STORAGE_TYPE_CEPH, 'Ceph'), + (REPLAY_STORAGE_TYPE_SWIFT, 'Swift'), + (REPLAY_STORAGE_TYPE_OSS, 'OSS'), + (REPLAY_STORAGE_TYPE_AZURE, 'Azure') +] + +REPLAY_STORAGE_TYPE_CHOICES = REPLAY_STORAGE_TYPE_CHOICES_DEFAULT + \ + REPLAY_STORAGE_TYPE_CHOICES_EXTENDS + + +# Command Storage + +COMMAND_STORAGE_TYPE_NULL = 'null' +COMMAND_STORAGE_TYPE_SERVER = 'server' +COMMAND_STORAGE_TYPE_ES = 'es' + +COMMAND_STORAGE_TYPE_EMPTY_FIELDS = [] +COMMAND_STORAGE_TYPE_ES_FIELDS = [ + {'name': 'HOSTS'}, + {'name': 'INDEX'}, + {'name': 'DOC_TYPE'} +] + +COMMAND_STORAGE_TYPE_FIELDS_MAP = { + COMMAND_STORAGE_TYPE_NULL: COMMAND_STORAGE_TYPE_EMPTY_FIELDS, + COMMAND_STORAGE_TYPE_SERVER: COMMAND_STORAGE_TYPE_EMPTY_FIELDS, + COMMAND_STORAGE_TYPE_ES: COMMAND_STORAGE_TYPE_ES_FIELDS, +} + +COMMAND_STORAGE_TYPE_CHOICES_DEFAULT = [ + (COMMAND_STORAGE_TYPE_NULL, 'Null'), + (COMMAND_STORAGE_TYPE_SERVER, 'Server'), +] + +COMMAND_STORAGE_TYPE_CHOICES_EXTENDS = [ + (COMMAND_STORAGE_TYPE_ES, 'Elasticsearch') +] + +COMMAND_STORAGE_TYPE_CHOICES = COMMAND_STORAGE_TYPE_CHOICES_DEFAULT + \ + COMMAND_STORAGE_TYPE_CHOICES_EXTENDS + diff --git a/apps/terminal/forms/__init__.py b/apps/terminal/forms/__init__.py new file mode 100644 index 000000000..23c06a94c --- /dev/null +++ b/apps/terminal/forms/__init__.py @@ -0,0 +1,5 @@ +# coding: utf-8 +# + +from .terminal import * +from .storage import * diff --git a/apps/terminal/forms/storage.py b/apps/terminal/forms/storage.py new file mode 100644 index 000000000..28092380e --- /dev/null +++ b/apps/terminal/forms/storage.py @@ -0,0 +1,166 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from terminal.models import ReplayStorage, CommandStorage + + +__all__ = [ + 'ReplayStorageAzureForm', 'ReplayStorageOSSForm', 'ReplayStorageS3Form', + 'ReplayStorageCephForm', 'ReplayStorageSwiftForm', + 'CommandStorageTypeESForm', +] + + +class BaseStorageForm(forms.Form): + + def __init__(self, *args, **kwargs): + super(BaseStorageForm, self).__init__(*args, **kwargs) + self.fields['type'].widget.attrs['disabled'] = True + self.fields.move_to_end('comment') + + +class BaseReplayStorageForm(BaseStorageForm, forms.ModelForm): + + class Meta: + model = ReplayStorage + fields = ['name', 'type', 'comment'] + + +class BaseCommandStorageForm(BaseStorageForm, forms.ModelForm): + + class Meta: + model = CommandStorage + fields = ['name', 'type', 'comment'] + + +class ReplayStorageAzureForm(BaseReplayStorageForm): + azure_container_name = forms.CharField( + max_length=128, label=_('Container name'), required=False + ) + azure_account_name = forms.CharField( + max_length=128, label=_('Account name'), required=False + ) + azure_account_key = forms.CharField( + max_length=128, label=_('Account key'), required=False, + widget=forms.PasswordInput + ) + azure_endpoint_suffix = forms.ChoiceField( + choices=( + ('core.chinacloudapi.cn', 'core.chinacloudapi.cn'), + ('core.windows.net', 'core.windows.net') + ), + label=_('Endpoint suffix'), required=False, + ) + + +class ReplayStorageOSSForm(BaseReplayStorageForm): + oss_bucket = forms.CharField( + max_length=128, label=_('Bucket'), required=False + ) + oss_access_key = forms.CharField( + max_length=128, label=_('Access key'), required=False, + widget=forms.PasswordInput + ) + oss_secret_key = forms.CharField( + max_length=128, label=_('Secret key'), required=False, + widget=forms.PasswordInput + ) + oss_endpoint = forms.CharField( + max_length=128, label=_('Endpoint'), required=False, + help_text=_( + """ + OSS: http://{REGION_NAME}.aliyuncs.com
    + Example: http://oss-cn-hangzhou.aliyuncs.com + """ + ) + ) + + +class ReplayStorageS3Form(BaseReplayStorageForm): + s3_bucket = forms.CharField( + max_length=128, label=_('Bucket'), required=False + ) + s3_access_key = forms.CharField( + max_length=128, label=_('Access key'), required=False, + widget=forms.PasswordInput + ) + s3_secret_key = forms.CharField( + max_length=128, label=_('Secret key'), required=False, + widget=forms.PasswordInput + ) + s3_endpoint = forms.CharField( + max_length=128, label=_('Endpoint'), required=False, + help_text=_( + """ + S3: http://s3.{REGION_NAME}.amazonaws.com
    + S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn
    + Example: http://s3.cn-north-1.amazonaws.com.cn + """ + ) + ) + + +class ReplayStorageCephForm(BaseReplayStorageForm): + ceph_bucket = forms.CharField( + max_length=128, label=_('Bucket'), required=False + ) + ceph_access_key = forms.CharField( + max_length=128, label=_('Access key'), required=False, + widget=forms.PasswordInput + ) + ceph_secret_key = forms.CharField( + max_length=128, label=_('Secret key'), required=False, + widget=forms.PasswordInput + ) + ceph_endpoint = forms.CharField( + max_length=128, label=_('Endpoint'), required=False, + help_text=_( + """ + S3: http://s3.{REGION_NAME}.amazonaws.com
    + S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn
    + Example: http://s3.cn-north-1.amazonaws.com.cn + """ + ) + ) + + +class ReplayStorageSwiftForm(BaseReplayStorageForm): + swift_bucket = forms.CharField( + max_length=128, label=_('Bucket'), required=False + ) + swift_access_key = forms.CharField( + max_length=128, label=_('Access key'), required=False, + widget=forms.PasswordInput + ) + swift_secret_key = forms.CharField( + max_length=128, label=_('Secret key'), required=False, + widget=forms.PasswordInput + ) + swift_region = forms.CharField( + max_length=128, label=_('Region'), required=False, + ) + swift_endpoint = forms.CharField( + max_length=128, label=_('Endpoint'), required=False, + ) + + +class CommandStorageTypeESForm(BaseCommandStorageForm): + es_hosts = forms.CharField( + max_length=128, label=_('Hosts'), required=True, + help_text=_( + """ + Tips: If there are multiple hosts, separate them with a comma (,) +
    + eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com + """ + ) + ) + es_index = forms.CharField( + max_length=128, label=_('Index'), required=True + ) + es_doc_type = forms.CharField( + max_length=128, label=_('Doc type'), required=True + ) diff --git a/apps/terminal/forms.py b/apps/terminal/forms/terminal.py similarity index 69% rename from apps/terminal/forms.py rename to apps/terminal/forms/terminal.py index b2f81fbb5..6051532d9 100644 --- a/apps/terminal/forms.py +++ b/apps/terminal/forms/terminal.py @@ -1,24 +1,22 @@ -# ~*~ coding: utf-8 ~*~ +# coding: utf-8 # +__all__ = ['TerminalForm'] + from django import forms from django.utils.translation import ugettext_lazy as _ -from .models import Terminal +from ..models import Terminal, ReplayStorage, CommandStorage def get_all_command_storage(): - from common import utils - command_storage = utils.get_command_storage_setting() - for k, v in command_storage.items(): - yield (k, k) + for c in CommandStorage.objects.all(): + yield (c.name, c.name) def get_all_replay_storage(): - from common import utils - replay_storage = utils.get_replay_storage_setting() - for k, v in replay_storage.items(): - yield (k, k) + for r in ReplayStorage.objects.all(): + yield (r.name, r.name) class TerminalForm(forms.ModelForm): diff --git a/apps/terminal/migrations/0016_commandstorage_replaystorage.py b/apps/terminal/migrations/0016_commandstorage_replaystorage.py new file mode 100644 index 000000000..108112fa4 --- /dev/null +++ b/apps/terminal/migrations/0016_commandstorage_replaystorage.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.5 on 2019-11-22 10:07 + +import common.fields.model +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0015_auto_20190923_1529'), + ] + + operations = [ + migrations.CreateModel( + name='CommandStorage', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=32, unique=True, verbose_name='Name')), + ('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('es', 'Elasticsearch')], default='server', max_length=16, verbose_name='Type')), + ('meta', common.fields.model.EncryptJsonDictTextField(default={})), + ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ReplayStorage', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=32, unique=True, verbose_name='Name')), + ('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure')], default='server', max_length=16, verbose_name='Type')), + ('meta', common.fields.model.EncryptJsonDictTextField(default={})), + ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/terminal/migrations/0017_auto_20191125_0931.py b/apps/terminal/migrations/0017_auto_20191125_0931.py new file mode 100644 index 000000000..c555e6db6 --- /dev/null +++ b/apps/terminal/migrations/0017_auto_20191125_0931.py @@ -0,0 +1,85 @@ +# Generated by Django 2.2.5 on 2019-11-25 01:31 + +from django.db import migrations + + +def get_storage_data(s): + from common.utils import signer + import json + value = s.value + encrypted = s.encrypted + if encrypted: + value = signer.unsign(value) + try: + value = json.loads(value) + except: + value = {} + return value + + +def get_setting(apps, schema_editor, key): + model = apps.get_model('settings', 'Setting') + db_alias = schema_editor.connection.alias + setting = model.objects.using(db_alias).filter(name=key) + if not setting: + return + return setting[0] + + +def init_storage_data(model): + model.objects.update_or_create( + name='null', type='null', + defaults={ + 'name': 'null', 'type': 'null', + 'comment': "Do not save" + } + ) + model.objects.update_or_create( + name='default', type='server', + defaults={ + 'name': 'default', 'type': 'server', + 'comment': "Store locally" + } + ) + + +def migrate_command_storage(apps, schema_editor): + model = apps.get_model("terminal", "CommandStorage") + init_storage_data(model) + + setting = get_setting(apps, schema_editor, "TERMINAL_COMMAND_STORAGE") + if not setting: + return + values = get_storage_data(setting) + for name, meta in values.items(): + tp = meta.pop("TYPE") + if not tp or name in ['default', 'null']: + continue + model.objects.create(name=name, type=tp, meta=meta) + + +def migrate_replay_storage(apps, schema_editor): + model = apps.get_model("terminal", "ReplayStorage") + init_storage_data(model) + + setting = get_setting(apps, schema_editor, "TERMINAL_REPLAY_STORAGE") + if not setting: + return + values = get_storage_data(setting) + for name, meta in values.items(): + tp = meta.pop("TYPE", None) + if not tp or name in ['default', 'null']: + continue + model.objects.create(name=name, type=tp, meta=meta) + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0016_commandstorage_replaystorage'), + ] + + operations = [ + migrations.RunPython(migrate_command_storage), + migrations.RunPython(migrate_replay_storage), + ] diff --git a/apps/terminal/migrations/0018_auto_20191202_1010.py b/apps/terminal/migrations/0018_auto_20191202_1010.py new file mode 100644 index 000000000..d3dd8bc4c --- /dev/null +++ b/apps/terminal/migrations/0018_auto_20191202_1010.py @@ -0,0 +1,67 @@ +# Generated by Django 2.2.7 on 2019-12-02 02:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0017_auto_20191125_0931'), + ] + + operations = [ + migrations.RemoveField( + model_name='session', + name='date_last_active', + ), + migrations.AlterField( + model_name='session', + name='remote_addr', + field=models.CharField(blank=True, max_length=128, null=True, + verbose_name='Remote addr'), + ), + migrations.AddField( + model_name='session', + name='asset_id', + field=models.CharField(blank=True, db_index=True, default='', + max_length=36), + ), + migrations.AddField( + model_name='session', + name='system_user_id', + field=models.CharField(blank=True, db_index=True, default='', + max_length=36), + ), + migrations.AddField( + model_name='session', + name='user_id', + field=models.CharField(blank=True, db_index=True, default='', + max_length=36), + ), + migrations.AlterField( + model_name='session', + name='asset', + field=models.CharField(db_index=True, max_length=1024, + verbose_name='Asset'), + ), + migrations.AlterField( + model_name='session', + name='protocol', + field=models.CharField( + choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'), + ('telnet', 'telnet')], db_index=True, default='ssh', + max_length=8), + ), + migrations.AlterField( + model_name='session', + name='system_user', + field=models.CharField(db_index=True, max_length=128, + verbose_name='System user'), + ), + migrations.AlterField( + model_name='session', + name='user', + field=models.CharField(db_index=True, max_length=128, + verbose_name='User'), + ), + ] diff --git a/apps/terminal/migrations/0019_auto_20191206_1000.py b/apps/terminal/migrations/0019_auto_20191206_1000.py new file mode 100644 index 000000000..069920085 --- /dev/null +++ b/apps/terminal/migrations/0019_auto_20191206_1000.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-12-06 02:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0018_auto_20191202_1010'), + ] + + operations = [ + migrations.AlterField( + model_name='replaystorage', + name='type', + field=models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure')], default='server', max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/terminal/migrations/0020_auto_20191218_1721.py b/apps/terminal/migrations/0020_auto_20191218_1721.py new file mode 100644 index 000000000..d9c6bfa5b --- /dev/null +++ b/apps/terminal/migrations/0020_auto_20191218_1721.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.11 on 2019-12-18 09:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0019_auto_20191206_1000'), + ] + + operations = [ + migrations.AlterField( + model_name='session', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'), ('telnet', 'telnet'), ('mysql', 'mysql')], db_index=True, default='ssh', max_length=8), + ), + ] diff --git a/apps/terminal/models.py b/apps/terminal/models.py index f7f2994ed..372544510 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import os import uuid +import jms_storage from django.db import models from django.db.models.signals import post_save @@ -13,9 +14,11 @@ from django.core.cache import cache from users.models import User from orgs.mixins.models import OrgModelMixin -from common.utils import get_command_storage_setting, get_replay_storage_setting +from common.mixins import CommonModelMixin +from common.fields.model import EncryptJsonDictTextField from .backends import get_multi_command_storage from .backends.command.models import AbstractSessionCommand +from . import const class Terminal(models.Model): @@ -55,21 +58,37 @@ class Terminal(models.Model): self.user.is_active = active self.user.save() - def get_command_storage_setting(self): - storage_all = get_command_storage_setting() - if self.command_storage in storage_all: - storage = storage_all.get(self.command_storage) + def get_command_storage(self): + storage = CommandStorage.objects.filter(name=self.command_storage).first() + return storage + + def get_command_storage_config(self): + s = self.get_command_storage() + if s: + config = s.config else: - storage = storage_all.get('default') - return {"TERMINAL_COMMAND_STORAGE": storage} + config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE + return config + + def get_command_storage_setting(self): + config = self.get_command_storage_config() + return {"TERMINAL_COMMAND_STORAGE": config} + + def get_replay_storage(self): + storage = ReplayStorage.objects.filter(name=self.replay_storage).first() + return storage + + def get_replay_storage_config(self): + s = self.get_replay_storage() + if s: + config = s.config + else: + config = settings.DEFAULT_TERMINAL_REPLAY_STORAGE + return config def get_replay_storage_setting(self): - storage_all = get_replay_storage_setting() - if self.replay_storage in storage_all: - storage = storage_all.get(self.replay_storage) - else: - storage = storage_all.get('default') - return {"TERMINAL_REPLAY_STORAGE": storage} + config = self.get_replay_storage_config() + return {"TERMINAL_REPLAY_STORAGE": config} @property def config(self): @@ -150,20 +169,23 @@ class Session(OrgModelMixin): ('rdp', 'rdp'), ('vnc', 'vnc'), ('telnet', 'telnet'), + ('mysql', 'mysql'), ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_("User")) - asset = models.CharField(max_length=1024, verbose_name=_("Asset")) - system_user = models.CharField(max_length=128, verbose_name=_("System user")) + user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True) + user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) + asset = models.CharField(max_length=1024, verbose_name=_("Asset"), db_index=True) + asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True) + system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True) + system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST") - remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) is_finished = models.BooleanField(default=False) has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) has_command = models.BooleanField(default=False, verbose_name=_("Command")) terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL) - protocol = models.CharField(choices=PROTOCOL_CHOICES, default='ssh', max_length=8) - date_last_active = models.DateTimeField(verbose_name=_("Date last active"), default=timezone.now) + protocol = models.CharField(choices=PROTOCOL_CHOICES, default='ssh', max_length=8, db_index=True) date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) @@ -229,7 +251,7 @@ class Session(OrgModelMixin): return cls.objects.filter(is_finished=False) def is_active(self): - if self.protocol in ['ssh', 'telnet']: + if self.protocol in ['ssh', 'telnet', 'rdp', 'mysql']: key = self.ACTIVE_CACHE_KEY_PREFIX.format(self.id) return bool(cache.get(key)) return True @@ -282,3 +304,89 @@ class Command(AbstractSessionCommand): class Meta: db_table = "terminal_command" ordering = ('-timestamp',) + + +class CommandStorage(CommonModelMixin): + TYPE_CHOICES = const.COMMAND_STORAGE_TYPE_CHOICES + TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys() + TYPE_SERVER = const.COMMAND_STORAGE_TYPE_SERVER + + name = models.CharField(max_length=32, verbose_name=_("Name"), unique=True) + type = models.CharField( + max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'), + default=TYPE_SERVER + ) + meta = EncryptJsonDictTextField(default={}) + comment = models.TextField( + max_length=128, default='', blank=True, verbose_name=_('Comment') + ) + + def __str__(self): + return self.name + + @property + def config(self): + config = self.meta + config.update({'TYPE': self.type}) + return config + + def in_defaults(self): + return self.type in self.TYPE_DEFAULTS + + def is_valid(self): + if self.in_defaults(): + return True + storage = jms_storage.get_log_storage(self.config) + return storage.ping() + + def can_delete(self): + return not self.in_defaults() + + +class ReplayStorage(CommonModelMixin): + TYPE_CHOICES = const.REPLAY_STORAGE_TYPE_CHOICES + TYPE_SERVER = const.REPLAY_STORAGE_TYPE_SERVER + TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys() + + name = models.CharField(max_length=32, verbose_name=_("Name"), unique=True) + type = models.CharField( + max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'), + default=TYPE_SERVER + ) + meta = EncryptJsonDictTextField(default={}) + comment = models.TextField( + max_length=128, default='', blank=True, verbose_name=_('Comment') + ) + + def __str__(self): + return self.name + + def convert_type(self): + s3_type_list = [ + const.REPLAY_STORAGE_TYPE_CEPH, const.REPLAY_STORAGE_TYPE_SWIFT + ] + tp = self.type + if tp in s3_type_list: + tp = const.REPLAY_STORAGE_TYPE_S3 + return tp + + @property + def config(self): + config = self.meta + config.update({'TYPE': self.convert_type()}) + return config + + def in_defaults(self): + return self.type in self.TYPE_DEFAULTS + + def is_valid(self): + if self.in_defaults(): + return True + storage = jms_storage.get_object_storage(self.config) + target = 'tests.py' + src = os.path.join(settings.BASE_DIR, 'common', target) + return storage.is_valid(src, target) + + def can_delete(self): + return not self.in_defaults() + diff --git a/apps/terminal/serializers/__init__.py b/apps/terminal/serializers/__init__.py index e198ec278..83e851bae 100644 --- a/apps/terminal/serializers/__init__.py +++ b/apps/terminal/serializers/__init__.py @@ -1,3 +1,5 @@ # -*- coding: utf-8 -*- # -from .v1 import * +from .terminal import * +from .session import * +from .storage import * diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py new file mode 100644 index 000000000..8d71b2a95 --- /dev/null +++ b/apps/terminal/serializers/session.py @@ -0,0 +1,24 @@ +from rest_framework import serializers + +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.serializers import AdaptedBulkListSerializer +from ..models import Session + + +class SessionSerializer(BulkOrgResourceModelSerializer): + command_amount = serializers.IntegerField(read_only=True) + org_id = serializers.CharField(allow_blank=True) + + class Meta: + model = Session + list_serializer_class = AdaptedBulkListSerializer + fields = [ + "id", "user", "asset", "system_user", "login_from", + "login_from_display", "remote_addr", "is_finished", + "has_replay", "can_replay", "protocol", "date_start", "date_end", + "terminal", "command_amount", + ] + + +class ReplaySerializer(serializers.Serializer): + file = serializers.FileField(allow_empty_file=True) diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py new file mode 100644 index 000000000..ea988e597 --- /dev/null +++ b/apps/terminal/serializers/storage.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +import copy +from rest_framework import serializers + +from common.fields.serializer import CustomMetaDictField +from ..models import ReplayStorage, CommandStorage +from .. import const + + +class ReplayStorageMetaDictField(CustomMetaDictField): + type_fields_map = const.REPLAY_STORAGE_TYPE_FIELDS_MAP + default_type = const.REPLAY_STORAGE_TYPE_SERVER + convert_key_remove_type_prefix = True + convert_key_to_upper = True + + +class BaseStorageSerializerMixin: + type_fields_map = None + + def process_meta(self, instance, validated_data): + new_meta = copy.deepcopy(validated_data.get('meta', {})) + tp = validated_data.get('type', '') + + if tp != instance.type: + return new_meta + + old_meta = instance.meta + fields = self.type_fields_map.get(instance.type, []) + for field in fields: + if not field.get('write_only', False): + continue + field_name = field['name'] + new_value = new_meta.get(field_name, '') + old_value = old_meta.get(field_name, '') + field_value = new_value if new_value else old_value + new_meta[field_name] = field_value + + return new_meta + + def update(self, instance, validated_data): + meta = self.process_meta(instance, validated_data) + validated_data['meta'] = meta + return super().update(instance, validated_data) + + +class ReplayStorageSerializer(BaseStorageSerializerMixin, + serializers.ModelSerializer): + + meta = ReplayStorageMetaDictField() + + type_fields_map = const.REPLAY_STORAGE_TYPE_FIELDS_MAP + + class Meta: + model = ReplayStorage + fields = ['id', 'name', 'type', 'meta', 'comment'] + + +class CommandStorageMetaDictField(CustomMetaDictField): + type_fields_map = const.COMMAND_STORAGE_TYPE_FIELDS_MAP + default_type = const.COMMAND_STORAGE_TYPE_SERVER + convert_key_remove_type_prefix = True + convert_key_to_upper = True + + +class CommandStorageSerializer(BaseStorageSerializerMixin, + serializers.ModelSerializer): + + meta = CommandStorageMetaDictField() + + type_fields_map = const.COMMAND_STORAGE_TYPE_FIELDS_MAP + + class Meta: + model = CommandStorage + fields = ['id', 'name', 'type', 'meta', 'comment'] diff --git a/apps/terminal/serializers/v1.py b/apps/terminal/serializers/terminal.py similarity index 55% rename from apps/terminal/serializers/v1.py rename to apps/terminal/serializers/terminal.py index 96560e374..1df4b40e6 100644 --- a/apps/terminal/serializers/v1.py +++ b/apps/terminal/serializers/terminal.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- -# from rest_framework import serializers -from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.mixins import BulkSerializerMixin from common.serializers import AdaptedBulkListSerializer -from ..models import Terminal, Status, Session, Task +from ..models import ( + Terminal, Status, Session, Task +) class TerminalSerializer(serializers.ModelSerializer): @@ -25,21 +24,6 @@ class TerminalSerializer(serializers.ModelSerializer): return Session.objects.filter(terminal=obj, is_finished=False).count() -class SessionSerializer(BulkOrgResourceModelSerializer): - command_amount = serializers.IntegerField(read_only=True) - org_id = serializers.CharField(allow_blank=True) - - class Meta: - model = Session - list_serializer_class = AdaptedBulkListSerializer - fields = [ - "id", "user", "asset", "system_user", "login_from", - "login_from_display", "remote_addr", "is_finished", - "has_replay", "can_replay", "protocol", "date_start", "date_end", - "terminal", "command_amount", - ] - - class StatusSerializer(serializers.ModelSerializer): class Meta: fields = ['id', 'terminal'] @@ -52,9 +36,3 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer): fields = '__all__' model = Task list_serializer_class = AdaptedBulkListSerializer - - -class ReplaySerializer(serializers.Serializer): - file = serializers.FileField(allow_empty_file=True) - - diff --git a/apps/terminal/templates/terminal/base_storage_create_update.html b/apps/terminal/templates/terminal/base_storage_create_update.html new file mode 100644 index 000000000..e432ecfda --- /dev/null +++ b/apps/terminal/templates/terminal/base_storage_create_update.html @@ -0,0 +1,59 @@ +{% extends '_base_create_update.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} + +{% block form %} +
    + {% bootstrap_form form layout="horizontal"%} +
    +
    +
    + + +
    +
    + +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/terminal/templates/terminal/base_storage_list.html b/apps/terminal/templates/terminal/base_storage_list.html new file mode 100644 index 000000000..40e93664b --- /dev/null +++ b/apps/terminal/templates/terminal/base_storage_list.html @@ -0,0 +1,130 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block content %} +
    +
    +
    + +
    +
    +
    +
    + + + +
    + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'Type' %}{% trans 'Comment' %}{% trans 'Action' %}
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} + diff --git a/apps/terminal/templates/terminal/command_list.html b/apps/terminal/templates/terminal/command_list.html index 1fc6f1d76..5e2dcddc9 100644 --- a/apps/terminal/templates/terminal/command_list.html +++ b/apps/terminal/templates/terminal/command_list.html @@ -168,7 +168,7 @@ $(document).ready(function () { function format(d) { - var output = $("
    ");
    +    var output = $("
    ");
     
         output.append('$ ', d.input);
         output.append('\r\n\r\n');
    diff --git a/apps/terminal/templates/terminal/command_storage_create_update.html b/apps/terminal/templates/terminal/command_storage_create_update.html
    new file mode 100644
    index 000000000..8abb976ae
    --- /dev/null
    +++ b/apps/terminal/templates/terminal/command_storage_create_update.html
    @@ -0,0 +1,30 @@
    +{% extends 'terminal/base_storage_create_update.html' %}
    +{% load i18n static %}
    +
    +{% block custom_foot_js %}
    +{{ block.super }}
    +
    +
    +{% endblock %}
    diff --git a/apps/terminal/templates/terminal/command_storage_list.html b/apps/terminal/templates/terminal/command_storage_list.html
    new file mode 100644
    index 000000000..724425cca
    --- /dev/null
    +++ b/apps/terminal/templates/terminal/command_storage_list.html
    @@ -0,0 +1,15 @@
    +{% extends 'terminal/base_storage_list.html' %}
    +{% load i18n static %}
    +
    +{% block create_storage_url %}{% url "terminal:command-storage-create" %}{% endblock%}
    +{% block create_storage_info %}{% trans 'Create command storage' %}{% endblock %}
    +{% block custom_foot_js %}
    +{{ block.super }}
    +
    +{% endblock %}
    +
    diff --git a/apps/terminal/templates/terminal/replay_storage_create_update.html b/apps/terminal/templates/terminal/replay_storage_create_update.html
    new file mode 100644
    index 000000000..878301d93
    --- /dev/null
    +++ b/apps/terminal/templates/terminal/replay_storage_create_update.html
    @@ -0,0 +1,25 @@
    +{% extends 'terminal/base_storage_create_update.html' %}
    +{% load i18n static %}
    +
    +{% block custom_foot_js %}
    +{{ block.super }}
    +
    +
    +{% endblock %}
    diff --git a/apps/terminal/templates/terminal/replay_storage_list.html b/apps/terminal/templates/terminal/replay_storage_list.html
    new file mode 100644
    index 000000000..786f4c844
    --- /dev/null
    +++ b/apps/terminal/templates/terminal/replay_storage_list.html
    @@ -0,0 +1,14 @@
    +{% extends 'terminal/base_storage_list.html' %}
    +{% load i18n static %}
    +
    +{% block create_storage_url %}{% url "terminal:replay-storage-create" %}{% endblock%}
    +{% block create_storage_info %}{% trans 'Create replay storage' %}{% endblock %}
    +{% block custom_foot_js %}
    +{{ block.super }}
    +
    +{% endblock %}
    diff --git a/apps/terminal/templates/terminal/session_detail.html b/apps/terminal/templates/terminal/session_detail.html
    index 1c96e3ee7..ea5ba9b00 100644
    --- a/apps/terminal/templates/terminal/session_detail.html
    +++ b/apps/terminal/templates/terminal/session_detail.html
    @@ -55,7 +55,7 @@
                                                 
                                                     {{ forloop.counter }}
                                                     {{ command.input | truncatechars:40 }}
    -                                                
    +                                                
     $ {{ command.input }}
     
     {{ command.output }}
    @@ -136,7 +136,13 @@ $ {{ command.input }}
                     }, 300)
                 }
                 var the_url = "{% url 'api-terminal:tasks-list' %}";
    -            requestApi({url: the_url, method: 'POST', body: JSON.stringify(data), success: success, success_message: 'Terminate success'});
    +            requestApi({
    +                url: the_url,
    +                method: 'POST',
    +                body: JSON.stringify(data),
    +                success: success,
    +                success_message: "{% trans 'Terminate success'%}"
    +            });
             }
             $(document).ready(function () {
                 $('.footable').footable();
    diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html
    index 1393bd241..965b8f556 100644
    --- a/apps/terminal/templates/terminal/session_list.html
    +++ b/apps/terminal/templates/terminal/session_list.html
    @@ -5,8 +5,6 @@
     {% load common_tags %}
     {% block custom_head_css_js %}
         
    -    
    -    
     {% endblock %}
     
     {% block content_left_head %}
    diff --git a/apps/terminal/templates/terminal/terminal_list.html b/apps/terminal/templates/terminal/terminal_list.html
    index 53325694b..f77949bfa 100644
    --- a/apps/terminal/templates/terminal/terminal_list.html
    +++ b/apps/terminal/templates/terminal/terminal_list.html
    @@ -18,6 +18,7 @@
     {% block table_search %}{% endblock %}
     
     {% block table_container %}
    +
     
    diff --git a/apps/terminal/templates/terminal/terminal_update.html b/apps/terminal/templates/terminal/terminal_update.html
    index f9554fda4..e140cd678 100644
    --- a/apps/terminal/templates/terminal/terminal_update.html
    +++ b/apps/terminal/templates/terminal/terminal_update.html
    @@ -3,8 +3,6 @@
     {% load static %}
     {% load bootstrap3 %}
     {% block custom_head_css_js %}
    -    
    -    
     {% endblock %}
     
    diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py
    index 96db7d31f..8c19fa685 100644
    --- a/apps/terminal/urls/api_urls.py
    +++ b/apps/terminal/urls/api_urls.py
    @@ -18,6 +18,8 @@ router.register(r'terminals', api.TerminalViewSet, 'terminal')
     router.register(r'tasks', api.TaskViewSet, 'tasks')
     router.register(r'commands', api.CommandViewSet, 'command')
     router.register(r'status', api.StatusViewSet, 'status')
    +router.register(r'replay-storages', api.ReplayStorageViewSet, 'replay-storage')
    +router.register(r'command-storages', api.CommandStorageViewSet, 'command-storage')
     
     urlpatterns = [
         path('sessions//replay/',
    @@ -27,7 +29,9 @@ urlpatterns = [
         path('terminals//access-key/', api.TerminalTokenApi.as_view(),
              name='terminal-access-key'),
         path('terminals/config/', api.TerminalConfig.as_view(), name='terminal-config'),
    -    path('commands/export/', api.CommandExportApi.as_view(), name="command-export")
    +    path('commands/export/', api.CommandExportApi.as_view(), name="command-export"),
    +    path('replay-storages//test-connective/', api.ReplayStorageTestConnectiveApi.as_view(), name='replay-storage-test-connective'),
    +    path('command-storages//test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective')
         # v2: get session's replay
         # path('v2/sessions//replay/',
         #     api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}),
    diff --git a/apps/terminal/urls/views_urls.py b/apps/terminal/urls/views_urls.py
    index 61db49c45..5caec1e75 100644
    --- a/apps/terminal/urls/views_urls.py
    +++ b/apps/terminal/urls/views_urls.py
    @@ -26,4 +26,14 @@ urlpatterns = [
         # Command view
         path('command/', views.CommandListView.as_view(), name='command-list'),
     
    +    # replay-storage
    +    path('terminal/replay-storage/', views.ReplayStorageListView.as_view(), name='replay-storage-list'),
    +    path('terminal/replay-storage/create/', views.ReplayStorageCreateView.as_view(), name='replay-storage-create'),
    +    path('terminal/replay-storage//update/', views.ReplayStorageUpdateView.as_view(), name='replay-storage-update'),
    +
    +    # command-storage
    +    path('terminal/command-storage/', views.CommandStorageListView.as_view(), name='command-storage-list'),
    +    path('terminal/command-storage/create/', views.CommandStorageCreateView.as_view(), name='command-storage-create'),
    +    path('terminal/command-storage//update/', views.CommandStorageUpdateView.as_view(), name='command-storage-update'),
    +
     ]
    diff --git a/apps/terminal/views/__init__.py b/apps/terminal/views/__init__.py
    index 8267a40bc..a63c3cf9a 100644
    --- a/apps/terminal/views/__init__.py
    +++ b/apps/terminal/views/__init__.py
    @@ -3,3 +3,7 @@
     from .terminal import *
     from .session import *
     from .command import *
    +from .storage import *
    +
    +# from .replay_storage import *
    +# from .command_storage import *
    diff --git a/apps/terminal/views/storage.py b/apps/terminal/views/storage.py
    new file mode 100644
    index 000000000..778cdd489
    --- /dev/null
    +++ b/apps/terminal/views/storage.py
    @@ -0,0 +1,181 @@
    +# coding: utf-8
    +#
    +
    +from django.http import Http404
    +from django.views.generic import TemplateView
    +from django.views.generic.edit import CreateView, UpdateView
    +from django.utils.translation import ugettext as _
    +
    +from common.permissions import PermissionsMixin, IsSuperUser
    +from terminal.models import ReplayStorage, CommandStorage
    +from .. import forms, const
    +
    +
    +__all__ = [
    +    'ReplayStorageListView', 'ReplayStorageCreateView',
    +    'ReplayStorageUpdateView', 'CommandStorageListView',
    +    'CommandStorageCreateView', 'CommandStorageUpdateView'
    +]
    +
    +
    +class ReplayStorageListView(PermissionsMixin, TemplateView):
    +    template_name = 'terminal/replay_storage_list.html'
    +    permission_classes = [IsSuperUser]
    +
    +    def get_context_data(self, **kwargs):
    +        context = {
    +            'app': _('Terminal'),
    +            'action': _('Replay storage list'),
    +            'is_replay': True,
    +            'type_choices': const.REPLAY_STORAGE_TYPE_CHOICES_EXTENDS,
    +        }
    +        kwargs.update(context)
    +        return super().get_context_data(**kwargs)
    +
    +
    +class CommandStorageListView(PermissionsMixin, TemplateView):
    +    template_name = 'terminal/command_storage_list.html'
    +    permission_classes = [IsSuperUser]
    +
    +    def get_context_data(self, **kwargs):
    +        context = {
    +            'app': _('Terminal'),
    +            'action': _('Command storage list'),
    +            'type_choices': const.COMMAND_STORAGE_TYPE_CHOICES_EXTENDS,
    +            'is_command': True,
    +        }
    +        kwargs.update(context)
    +        return super().get_context_data(**kwargs)
    +
    +
    +class BaseStorageCreateUpdateViewMixin:
    +    permission_classes = [IsSuperUser]
    +    default_type = None
    +    form_class = None
    +    form_class_choices = {}
    +
    +    def get_initial(self):
    +        return {'type': self.get_type()}
    +
    +    def get_type(self):
    +        return self.default_type
    +
    +    def get_form_class(self):
    +        tp = self.get_type()
    +        form_class = self.form_class_choices.get(tp)
    +        if not form_class:
    +            raise Http404()
    +        return form_class
    +
    +
    +class ReplayStorageCreateUpdateViewMixin(BaseStorageCreateUpdateViewMixin):
    +    model = ReplayStorage
    +    default_type = const.REPLAY_STORAGE_TYPE_S3
    +    form_class = forms.ReplayStorageS3Form
    +    form_class_choices = {
    +        const.REPLAY_STORAGE_TYPE_S3: forms.ReplayStorageS3Form,
    +        const.REPLAY_STORAGE_TYPE_CEPH: forms.ReplayStorageCephForm,
    +        const.REPLAY_STORAGE_TYPE_SWIFT: forms.ReplayStorageSwiftForm,
    +        const.REPLAY_STORAGE_TYPE_OSS: forms.ReplayStorageOSSForm,
    +        const.REPLAY_STORAGE_TYPE_AZURE: forms.ReplayStorageAzureForm
    +    }
    +
    +
    +class ReplayStorageCreateView(ReplayStorageCreateUpdateViewMixin,
    +                              PermissionsMixin, CreateView):
    +    template_name = 'terminal/replay_storage_create_update.html'
    +
    +    def get_type(self):
    +        tp = self.request.GET.get("type")
    +        if tp:
    +            return tp.lower()
    +        return super().get_type()
    +
    +    def get_context_data(self, **kwargs):
    +        context = {
    +            'app': _('Terminal'),
    +            'action': _('Create replay storage'),
    +            'api_action': 'create'
    +        }
    +        kwargs.update(context)
    +        return super().get_context_data(**kwargs)
    +
    +
    +class ReplayStorageUpdateView(ReplayStorageCreateUpdateViewMixin,
    +                              PermissionsMixin, UpdateView):
    +    template_name = 'terminal/replay_storage_create_update.html'
    +
    +    def get_initial(self):
    +        initial_data = super().get_initial()
    +        for k, v in self.object.meta.items():
    +            _k = "{}_{}".format(self.object.type, k.lower())
    +            initial_data[_k] = v
    +        return initial_data
    +
    +    def get_type(self):
    +        return self.object.type
    +
    +    def get_context_data(self, **kwargs):
    +        context = {
    +            'app': _('Terminal'),
    +            'action': _('Update replay storage'),
    +            'api_action': 'update'
    +        }
    +        kwargs.update(context)
    +        return super().get_context_data(**kwargs)
    +
    +
    +class CommandStorageCreateUpdateViewMixin(BaseStorageCreateUpdateViewMixin):
    +    model = CommandStorage
    +    default_type = const.COMMAND_STORAGE_TYPE_ES
    +    form_class = forms.CommandStorageTypeESForm
    +    form_class_choices = {
    +        const.COMMAND_STORAGE_TYPE_ES: forms.CommandStorageTypeESForm
    +    }
    +
    +
    +class CommandStorageCreateView(CommandStorageCreateUpdateViewMixin,
    +                               PermissionsMixin, CreateView):
    +    template_name = 'terminal/command_storage_create_update.html'
    +
    +    def get_type(self):
    +        tp = self.request.GET.get("type")
    +        if tp:
    +            return tp.lower()
    +        return super().get_type()
    +
    +    def get_context_data(self, **kwargs):
    +        context = {
    +            'app': _('Terminal'),
    +            'action': _('Create command storage'),
    +            'api_action': 'create'
    +        }
    +        kwargs.update(context)
    +        return super().get_context_data(**kwargs)
    +
    +
    +class CommandStorageUpdateView(CommandStorageCreateUpdateViewMixin,
    +                               PermissionsMixin, UpdateView):
    +    template_name = 'terminal/command_storage_create_update.html'
    +
    +    def get_initial(self):
    +        initial_data = super().get_initial()
    +        for k, v in self.object.meta.items():
    +            _k = "{}_{}".format(self.object.type, k.lower())
    +            if k == 'HOSTS':
    +                v = ','.join(v)
    +            initial_data[_k] = v
    +        return initial_data
    +
    +    def get_type(self):
    +        return self.object.type
    +
    +    def get_context_data(self, **kwargs):
    +        context = {
    +            'app': _('Terminal'),
    +            'action': _('Update command storage'),
    +            'api_action': 'update'
    +        }
    +        kwargs.update(context)
    +        return super().get_context_data(**kwargs)
    +
    diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py
    index 1706c38a7..5e3d90701 100644
    --- a/apps/tickets/api/ticket.py
    +++ b/apps/tickets/api/ticket.py
    @@ -13,7 +13,7 @@ class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
         serializer_class = serializers.TicketSerializer
         queryset = models.Ticket.objects.all()
         permission_classes = (IsValidUser,)
    -    filter_fields = ['status', 'title', 'action']
    +    filter_fields = ['status', 'title', 'action', 'user_display']
         search_fields = ['user_display', 'title']
     
     
    diff --git a/apps/tickets/serializers/ticket.py b/apps/tickets/serializers/ticket.py
    index f148c6375..e6ca13a2b 100644
    --- a/apps/tickets/serializers/ticket.py
    +++ b/apps/tickets/serializers/ticket.py
    @@ -32,8 +32,6 @@ class TicketSerializer(serializers.ModelSerializer):
             if action and user not in instance.assignees.all():
                 error = {"action": "Only assignees can update"}
                 raise serializers.ValidationError(error)
    -        print(validated_data)
    -        print(instance.status)
             if instance.status == instance.STATUS_CLOSED:
                 validated_data.pop('action')
             instance = super().update(instance, validated_data)
    diff --git a/apps/users/api/group.py b/apps/users/api/group.py
    index eb00ea220..860ca36b4 100644
    --- a/apps/users/api/group.py
    +++ b/apps/users/api/group.py
    @@ -1,35 +1,19 @@
     # -*- coding: utf-8 -*-
     #
     
    -from ..serializers import (
    -    UserGroupSerializer,
    -    UserGroupListSerializer,
    -    UserGroupUpdateMemberSerializer,
    -)
    +from ..serializers import UserGroupSerializer
     from ..models import UserGroup
     from orgs.mixins.api import OrgBulkModelViewSet
    -from orgs.mixins import generics
     from common.permissions import IsOrgAdmin
     
     
    -__all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
    +__all__ = ['UserGroupViewSet']
     
     
     class UserGroupViewSet(OrgBulkModelViewSet):
         model = UserGroup
         filter_fields = ("name",)
         search_fields = filter_fields
    +    permission_classes = (IsOrgAdmin,)
         serializer_class = UserGroupSerializer
    -    permission_classes = (IsOrgAdmin,)
     
    -    def get_serializer_class(self):
    -        if self.action in ("list", 'retrieve') and \
    -                self.request.query_params.get("display"):
    -            return UserGroupListSerializer
    -        return self.serializer_class
    -
    -
    -class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
    -    model = UserGroup
    -    serializer_class = UserGroupUpdateMemberSerializer
    -    permission_classes = (IsOrgAdmin,)
    diff --git a/apps/users/api/relation.py b/apps/users/api/relation.py
    index a579419d5..fbab92ee9 100644
    --- a/apps/users/api/relation.py
    +++ b/apps/users/api/relation.py
    @@ -19,8 +19,8 @@ class UserUserGroupRelationViewSet(BulkModelViewSet):
     
         def get_queryset(self):
             queryset = User.groups.through.objects.all()\
    -            .annotate(user_name=F('user__name'))\
    -            .annotate(usergroup_name=F('usergroup__name'))
    +            .annotate(user_display=F('user__name'))\
    +            .annotate(usergroup_display=F('usergroup__name'))
             return queryset
     
         def allow_bulk_destroy(self, qs, filtered):
    diff --git a/apps/users/api/user.py b/apps/users/api/user.py
    index 3087658e2..98dcbd91c 100644
    --- a/apps/users/api/user.py
    +++ b/apps/users/api/user.py
    @@ -24,7 +24,7 @@ from ..signals import post_user_create
     
     logger = get_logger(__name__)
     __all__ = [
    -    'UserViewSet', 'UserChangePasswordApi', 'UserUpdateGroupApi',
    +    'UserViewSet', 'UserChangePasswordApi',
         'UserResetPasswordApi', 'UserResetPKApi', 'UserUpdatePKApi',
         'UserUnblockPKApi', 'UserProfileApi', 'UserResetOTPApi',
     ]
    @@ -39,8 +39,10 @@ class UserQuerysetMixin:
     class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
         filter_fields = ('username', 'email', 'name', 'id')
         search_fields = filter_fields
    -    serializer_class = serializers.UserSerializer
    -    serializer_display_class = serializers.UserDisplaySerializer
    +    serializer_classes = {
    +        'default': serializers.UserSerializer,
    +        'display': serializers.UserDisplaySerializer
    +    }
         permission_classes = (IsOrgAdmin, CanUpdateDeleteUser)
     
         def get_queryset(self):
    @@ -67,6 +69,12 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
                 self.permission_classes = (IsSuperUser,)
             return super().get_permissions()
     
    +    def perform_destroy(self, instance):
    +        if current_org.is_real():
    +            instance.remove()
    +        else:
    +            return super().perform_destroy(instance)
    +
         def perform_bulk_destroy(self, objects):
             for obj in objects:
                 self.check_object_permissions(self.request, obj)
    @@ -93,11 +101,6 @@ class UserChangePasswordApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView):
             user.save()
     
     
    -class UserUpdateGroupApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView):
    -    serializer_class = serializers.UserUpdateGroupSerializer
    -    permission_classes = (IsOrgAdmin,)
    -
    -
     class UserResetPasswordApi(UserQuerysetMixin, generics.UpdateAPIView):
         queryset = User.objects.all()
         serializer_class = serializers.UserSerializer
    @@ -176,5 +179,4 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
             if user.mfa_enabled:
                 user.reset_mfa()
                 user.save()
    -            logout(request)
             return Response({"msg": "success"})
    diff --git a/apps/users/forms/__init__.py b/apps/users/forms/__init__.py
    new file mode 100644
    index 000000000..8b4d5d888
    --- /dev/null
    +++ b/apps/users/forms/__init__.py
    @@ -0,0 +1,5 @@
    +# -*- coding: utf-8 -*-
    +#
    +from .user import *
    +from .group import *
    +from .profile import *
    diff --git a/apps/users/forms/group.py b/apps/users/forms/group.py
    new file mode 100644
    index 000000000..8d026a777
    --- /dev/null
    +++ b/apps/users/forms/group.py
    @@ -0,0 +1,44 @@
    +# -*- coding: utf-8 -*-
    +#
    +from django import forms
    +from django.utils.translation import gettext_lazy as _
    +
    +from orgs.mixins.forms import OrgModelForm
    +from ..models import User, UserGroup
    +
    +__all__ = ['UserGroupForm']
    +
    +
    +class UserGroupForm(OrgModelForm):
    +    users = forms.ModelMultipleChoiceField(
    +        queryset=User.objects.none(),
    +        label=_("User"),
    +        widget=forms.SelectMultiple(
    +            attrs={
    +                'class': 'users-select2',
    +                'data-placeholder': _('Select users')
    +            }
    +        ),
    +        required=False,
    +    )
    +
    +    def __init__(self, **kwargs):
    +        super().__init__(**kwargs)
    +        self.set_fields_queryset()
    +
    +    def set_fields_queryset(self):
    +        users_field = self.fields.get('users')
    +        if self.instance:
    +            users_field.initial = self.instance.users.all()
    +            users_field.queryset = self.instance.users.all()
    +        else:
    +            users_field.queryset = User.objects.none()
    +
    +    def save(self, commit=True):
    +        raise Exception("Save by restful api")
    +
    +    class Meta:
    +        model = UserGroup
    +        fields = [
    +            'name', 'users', 'comment',
    +        ]
    diff --git a/apps/users/forms/profile.py b/apps/users/forms/profile.py
    new file mode 100644
    index 000000000..bd1047733
    --- /dev/null
    +++ b/apps/users/forms/profile.py
    @@ -0,0 +1,152 @@
    +# -*- coding: utf-8 -*-
    +#
    +from django import forms
    +from django.utils.translation import gettext_lazy as _
    +from captcha.fields import CaptchaField
    +
    +from common.utils import validate_ssh_public_key
    +from ..models import User
    +
    +
    +__all__ = [
    +    'UserProfileForm', 'UserMFAForm', 'UserFirstLoginFinishForm',
    +    'UserPasswordForm', 'UserPublicKeyForm', 'FileForm',
    +    'UserTokenResetPasswordForm', 'UserForgotPasswordForm',
    +]
    +
    +
    +class UserProfileForm(forms.ModelForm):
    +    username = forms.CharField(disabled=True, label=_("Username"))
    +    name = forms.CharField(disabled=True, label=_("Name"))
    +    email = forms.CharField(disabled=True)
    +
    +    class Meta:
    +        model = User
    +        fields = [
    +            'username', 'name', 'email',
    +            'wechat', 'phone',
    +        ]
    +
    +
    +UserProfileForm.verbose_name = _("Profile")
    +
    +
    +class UserMFAForm(forms.ModelForm):
    +
    +    mfa_description = _(
    +        'When enabled, '
    +        'you will enter the MFA binding process the next time you log in. '
    +        'you can also directly bind in '
    +        '"personal information -> quick modification -> change MFA Settings"!')
    +
    +    class Meta:
    +        model = User
    +        fields = ['mfa_level']
    +        widgets = {'mfa_level': forms.RadioSelect()}
    +        help_texts = {
    +            'mfa_level': _('* Enable MFA authentication '
    +                           'to make the account more secure.'),
    +        }
    +
    +
    +UserMFAForm.verbose_name = _("MFA")
    +
    +
    +class UserFirstLoginFinishForm(forms.Form):
    +    finish_description = _(
    +        'In order to protect you and your company, '
    +        'please keep your account, '
    +        'password and key sensitive information properly. '
    +        '(for example: setting complex password, enabling MFA authentication)'
    +    )
    +
    +
    +UserFirstLoginFinishForm.verbose_name = _("Finish")
    +
    +
    +class UserTokenResetPasswordForm(forms.Form):
    +    new_password = forms.CharField(
    +        min_length=5, max_length=128,
    +        widget=forms.PasswordInput,
    +        label=_("New password")
    +    )
    +    confirm_password = forms.CharField(
    +        min_length=5, max_length=128,
    +        widget=forms.PasswordInput,
    +        label=_("Confirm password")
    +    )
    +
    +    def clean_confirm_password(self):
    +        new_password = self.cleaned_data['new_password']
    +        confirm_password = self.cleaned_data['confirm_password']
    +
    +        if new_password != confirm_password:
    +            raise forms.ValidationError(_('Password does not match'))
    +        return confirm_password
    +
    +
    +class UserForgotPasswordForm(forms.Form):
    +    email = forms.EmailField(label=_("Email"))
    +    captcha = CaptchaField(label=_("Captcha"))
    +
    +
    +class UserPasswordForm(UserTokenResetPasswordForm):
    +    old_password = forms.CharField(
    +        max_length=128, widget=forms.PasswordInput,
    +        label=_("Old password")
    +    )
    +
    +    def __init__(self, *args, **kwargs):
    +        self.instance = kwargs.pop('instance')
    +        super().__init__(*args, **kwargs)
    +
    +    def clean_old_password(self):
    +        old_password = self.cleaned_data['old_password']
    +        if not self.instance.check_password(old_password):
    +            raise forms.ValidationError(_('Old password error'))
    +        return old_password
    +
    +    def save(self):
    +        password = self.cleaned_data['new_password']
    +        self.instance.reset_password(new_password=password)
    +        return self.instance
    +
    +
    +class UserPublicKeyForm(forms.Form):
    +    pubkey_description = _('Automatically configure and download the SSH key')
    +    public_key = forms.CharField(
    +        label=_('ssh public key'), max_length=5000, required=False,
    +        widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}),
    +        help_text=_('Paste your id_rsa.pub here.')
    +    )
    +
    +    def __init__(self, *args, **kwargs):
    +        if 'instance' in kwargs:
    +            self.instance = kwargs.pop('instance')
    +        else:
    +            self.instance = None
    +        super().__init__(*args, **kwargs)
    +
    +    def clean_public_key(self):
    +        public_key = self.cleaned_data['public_key']
    +        if self.instance.public_key and public_key == self.instance.public_key:
    +            msg = _('Public key should not be the same as your old one.')
    +            raise forms.ValidationError(msg)
    +
    +        if public_key and not validate_ssh_public_key(public_key):
    +            raise forms.ValidationError(_('Not a valid ssh public key'))
    +        return public_key
    +
    +    def save(self):
    +        public_key = self.cleaned_data['public_key']
    +        if public_key:
    +            self.instance.public_key = public_key
    +            self.instance.save()
    +        return self.instance
    +
    +
    +UserPublicKeyForm.verbose_name = _("Public key")
    +
    +
    +class FileForm(forms.Form):
    +    file = forms.FileField()
    diff --git a/apps/users/forms.py b/apps/users/forms/user.py
    similarity index 52%
    rename from apps/users/forms.py
    rename to apps/users/forms/user.py
    index 6b360fb71..a58e1fef1 100644
    --- a/apps/users/forms.py
    +++ b/apps/users/forms/user.py
    @@ -1,39 +1,19 @@
    -# ~*~ coding: utf-8 ~*~
     
     from django import forms
     from django.utils.translation import gettext_lazy as _
    -from django.conf import settings
     
     from common.utils import validate_ssh_public_key
     from orgs.mixins.forms import OrgModelForm
    -from .models import User, UserGroup
    -from .utils import check_password_rules, get_current_org_members
    +from ..models import User
    +from ..utils import (
    +    check_password_rules, get_current_org_members, get_source_choices
    +)
     
     
    -class UserCheckPasswordForm(forms.Form):
    -    username = forms.CharField(label=_('Username'), max_length=100)
    -    password = forms.CharField(
    -        label=_('Password'), widget=forms.PasswordInput,
    -        max_length=128, strip=False
    -    )
    -
    -
    -class UserCheckOtpCodeForm(forms.Form):
    -    otp_code = forms.CharField(label=_('MFA code'), max_length=6)
    -
    -
    -def get_source_choices():
    -    choices_all = dict(User.SOURCE_CHOICES)
    -    choices = [
    -        (User.SOURCE_LOCAL, choices_all[User.SOURCE_LOCAL]),
    -    ]
    -    if settings.AUTH_LDAP:
    -        choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP]))
    -    if settings.AUTH_OPENID:
    -        choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID]))
    -    if settings.AUTH_RADIUS:
    -        choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS]))
    -    return choices
    +__all__ = [
    +    'UserCreateForm', 'UserUpdateForm', 'UserBulkUpdateForm',
    +    'UserCheckOtpCodeForm', 'UserCheckPasswordForm'
    +]
     
     
     class UserCreateUpdateFormMixin(OrgModelForm):
    @@ -157,131 +137,6 @@ class UserUpdateForm(UserCreateUpdateFormMixin):
         pass
     
     
    -class UserProfileForm(forms.ModelForm):
    -    username = forms.CharField(disabled=True, label=_("Username"))
    -    name = forms.CharField(disabled=True, label=_("Name"))
    -    email = forms.CharField(disabled=True)
    -
    -    class Meta:
    -        model = User
    -        fields = [
    -            'username', 'name', 'email',
    -            'wechat', 'phone',
    -        ]
    -
    -
    -UserProfileForm.verbose_name = _("Profile")
    -
    -
    -class UserMFAForm(forms.ModelForm):
    -
    -    mfa_description = _(
    -        'When enabled, '
    -        'you will enter the MFA binding process the next time you log in. '
    -        'you can also directly bind in '
    -        '"personal information -> quick modification -> change MFA Settings"!')
    -
    -    class Meta:
    -        model = User
    -        fields = ['mfa_level']
    -        widgets = {'mfa_level': forms.RadioSelect()}
    -        help_texts = {
    -            'mfa_level': _('* Enable MFA authentication '
    -                           'to make the account more secure.'),
    -        }
    -
    -
    -UserMFAForm.verbose_name = _("MFA")
    -
    -
    -class UserFirstLoginFinishForm(forms.Form):
    -    finish_description = _(
    -        'In order to protect you and your company, '
    -        'please keep your account, '
    -        'password and key sensitive information properly. '
    -        '(for example: setting complex password, enabling MFA authentication)'
    -    )
    -
    -
    -UserFirstLoginFinishForm.verbose_name = _("Finish")
    -
    -
    -class UserPasswordForm(forms.Form):
    -    old_password = forms.CharField(
    -        max_length=128, widget=forms.PasswordInput,
    -        label=_("Old password")
    -    )
    -    new_password = forms.CharField(
    -        min_length=5, max_length=128,
    -        widget=forms.PasswordInput,
    -        label=_("New password")
    -    )
    -    confirm_password = forms.CharField(
    -        min_length=5, max_length=128,
    -        widget=forms.PasswordInput,
    -        label=_("Confirm password")
    -    )
    -
    -    def __init__(self, *args, **kwargs):
    -        self.instance = kwargs.pop('instance')
    -        super().__init__(*args, **kwargs)
    -
    -    def clean_old_password(self):
    -        old_password = self.cleaned_data['old_password']
    -        if not self.instance.check_password(old_password):
    -            raise forms.ValidationError(_('Old password error'))
    -        return old_password
    -
    -    def clean_confirm_password(self):
    -        new_password = self.cleaned_data['new_password']
    -        confirm_password = self.cleaned_data['confirm_password']
    -
    -        if new_password != confirm_password:
    -            raise forms.ValidationError(_('Password does not match'))
    -        return confirm_password
    -
    -    def save(self):
    -        password = self.cleaned_data['new_password']
    -        self.instance.reset_password(new_password=password)
    -        return self.instance
    -
    -
    -class UserPublicKeyForm(forms.Form):
    -    pubkey_description = _('Automatically configure and download the SSH key')
    -    public_key = forms.CharField(
    -        label=_('ssh public key'), max_length=5000, required=False,
    -        widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}),
    -        help_text=_('Paste your id_rsa.pub here.')
    -    )
    -
    -    def __init__(self, *args, **kwargs):
    -        if 'instance' in kwargs:
    -            self.instance = kwargs.pop('instance')
    -        else:
    -            self.instance = None
    -        super().__init__(*args, **kwargs)
    -
    -    def clean_public_key(self):
    -        public_key = self.cleaned_data['public_key']
    -        if self.instance.public_key and public_key == self.instance.public_key:
    -            msg = _('Public key should not be the same as your old one.')
    -            raise forms.ValidationError(msg)
    -
    -        if public_key and not validate_ssh_public_key(public_key):
    -            raise forms.ValidationError(_('Not a valid ssh public key'))
    -        return public_key
    -
    -    def save(self):
    -        public_key = self.cleaned_data['public_key']
    -        if public_key:
    -            self.instance.public_key = public_key
    -            self.instance.save()
    -        return self.instance
    -
    -
    -UserPublicKeyForm.verbose_name = _("Public key")
    -
    -
     class UserBulkUpdateForm(OrgModelForm):
         users = forms.ModelMultipleChoiceField(
             required=True,
    @@ -333,40 +188,12 @@ class UserBulkUpdateForm(OrgModelForm):
             return users
     
     
    -class UserGroupForm(OrgModelForm):
    -    users = forms.ModelMultipleChoiceField(
    -        queryset=User.objects.none(),
    -        label=_("User"),
    -        widget=forms.SelectMultiple(
    -            attrs={
    -                'class': 'users-select2',
    -                'data-placeholder': _('Select users')
    -            }
    -        ),
    -        required=False,
    +class UserCheckPasswordForm(forms.Form):
    +    password = forms.CharField(
    +        label=_('Password'), widget=forms.PasswordInput,
    +        max_length=128, strip=False
         )
     
    -    def __init__(self, **kwargs):
    -        super().__init__(**kwargs)
    -        self.set_fields_queryset()
     
    -    def set_fields_queryset(self):
    -        users_field = self.fields.get('users')
    -        if self.instance:
    -            users_field.initial = self.instance.users.all()
    -            users_field.queryset = self.instance.users.all()
    -        else:
    -            users_field.queryset = User.objects.none()
    -
    -    def save(self, commit=True):
    -        raise Exception("Save by restful api")
    -
    -    class Meta:
    -        model = UserGroup
    -        fields = [
    -            'name', 'users', 'comment',
    -        ]
    -
    -
    -class FileForm(forms.Form):
    -    file = forms.FileField()
    +class UserCheckOtpCodeForm(forms.Form):
    +    otp_code = forms.CharField(label=_('MFA code'), max_length=6)
    diff --git a/apps/users/hands.py b/apps/users/hands.py
    index 0792fa099..5e2007c8a 100644
    --- a/apps/users/hands.py
    +++ b/apps/users/hands.py
    @@ -11,7 +11,6 @@
     """
     
     # from terminal.models import Terminal
    -# from audits.tasks import write_login_log_async
     # from users.models import User
     # from perms.models import AssetPermission
     # from perms.utils import get_user_granted_assets, get_user_granted_asset_groups
    diff --git a/apps/users/models/group.py b/apps/users/models/group.py
    index e211759bc..0ae636f76 100644
    --- a/apps/users/models/group.py
    +++ b/apps/users/models/group.py
    @@ -4,6 +4,7 @@ import uuid
     from django.db import models, IntegrityError
     from django.utils.translation import ugettext_lazy as _
     
    +from common.utils import lazyproperty
     from orgs.mixins.models import OrgModelMixin
     
     __all__ = ['UserGroup']
    @@ -20,6 +21,10 @@ class UserGroup(OrgModelMixin):
         def __str__(self):
             return self.name
     
    +    @lazyproperty
    +    def users_amount(self):
    +        return self.users.count()
    +
         class Meta:
             ordering = ['name']
             unique_together = [('org_id', 'name'),]
    diff --git a/apps/users/models/user.py b/apps/users/models/user.py
    index ba4668f7f..a062f6c99 100644
    --- a/apps/users/models/user.py
    +++ b/apps/users/models/user.py
    @@ -17,14 +17,13 @@ from django.utils import timezone
     from django.shortcuts import reverse
     
     from orgs.utils import current_org
    -from common.utils import get_signer, date_expired_default, get_logger, lazyproperty
    +from common.utils import signer, date_expired_default, get_logger, lazyproperty
     from common import fields
    +from ..signals import post_user_change_password
     
     
     __all__ = ['User']
     
    -signer = get_signer()
    -
     logger = get_logger(__file__)
     
     
    @@ -43,14 +42,10 @@ class AuthMixin:
             self.set_password(password_raw_)
     
         def set_password(self, raw_password):
    -        self._set_password = True
             if self.can_update_password():
                 self.date_password_last_updated = timezone.now()
    +            post_user_change_password.send(self.__class__, user=self)
                 super().set_password(raw_password)
    -        else:
    -            error = _("User auth from {}, go there change password").format(
    -                self.source)
    -            raise PermissionError(error)
     
         def can_update_password(self):
             return self.is_local
    @@ -196,22 +191,22 @@ class RoleMixin:
         def is_app(self):
             return self.role == 'App'
     
    -    @property
    +    @lazyproperty
         def user_orgs(self):
             from orgs.models import Organization
             return Organization.get_user_user_orgs(self)
     
    -    @property
    +    @lazyproperty
         def admin_orgs(self):
             from orgs.models import Organization
             return Organization.get_user_admin_orgs(self)
     
    -    @property
    +    @lazyproperty
         def audit_orgs(self):
             from orgs.models import Organization
             return Organization.get_user_audit_orgs(self)
     
    -    @property
    +    @lazyproperty
         def admin_or_audit_orgs(self):
             from orgs.models import Organization
             return Organization.get_user_admin_or_audit_orgs(self)
    @@ -223,26 +218,26 @@ class RoleMixin:
             else:
                 return False
     
    -    @property
    +    @lazyproperty
         def is_org_auditor(self):
             if self.is_super_auditor or self.related_audit_orgs.exists():
                 return True
             else:
                 return False
     
    -    @property
    +    @lazyproperty
         def can_admin_current_org(self):
             return current_org.can_admin_by(self)
     
    -    @property
    +    @lazyproperty
         def can_audit_current_org(self):
             return current_org.can_audit_by(self)
     
    -    @property
    +    @lazyproperty
         def can_user_current_org(self):
             return current_org.can_user_by(self)
     
    -    @property
    +    @lazyproperty
         def can_admin_or_audit_current_org(self):
             return self.can_admin_current_org or self.can_audit_current_org
     
    @@ -267,6 +262,16 @@ class RoleMixin:
             access_key = app.create_access_key()
             return app, access_key
     
    +    def remove(self):
    +        if not current_org.is_real():
    +            return
    +        if self.can_user_current_org:
    +            current_org.users.remove(self)
    +        if self.can_admin_current_org:
    +            current_org.admins.remove(self)
    +        if self.can_audit_current_org:
    +            current_org.auditors.remove(self)
    +
     
     class TokenMixin:
         CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
    @@ -384,7 +389,7 @@ class MFAMixin:
     
         @staticmethod
         def mfa_is_otp():
    -        if settings.CONFIG.OTP_IN_RADIUS:
    +        if settings.OTP_IN_RADIUS:
                 return False
             return True
     
    @@ -401,7 +406,7 @@ class MFAMixin:
             return check_otp_code(self.otp_secret_key, code)
     
         def check_mfa(self, code):
    -        if settings.CONFIG.OTP_IN_RADIUS:
    +        if settings.OTP_IN_RADIUS:
                 return self.check_radius(code)
             else:
                 return self.check_otp(code)
    @@ -540,6 +545,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
                 return True
             return False
     
    +    def set_avatar(self, f):
    +        self.avatar.save(self.username, f)
    +
         def avatar_url(self):
             admin_default = settings.STATIC_URL + "img/avatar/admin.png"
             user_default = settings.STATIC_URL + "img/avatar/user.png"
    diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py
    index c817de289..67b21668c 100644
    --- a/apps/users/serializers/group.py
    +++ b/apps/users/serializers/group.py
    @@ -4,29 +4,29 @@ from django.utils.translation import ugettext_lazy as _
     
     from rest_framework import serializers
     
    -from common.fields import StringManyToManyField
     from common.serializers import AdaptedBulkListSerializer
     from orgs.mixins.serializers import BulkOrgResourceModelSerializer
    +from django.db.models import Count
     from ..models import User, UserGroup
     from .. import utils
     
     __all__ = [
    -    'UserGroupSerializer', 'UserGroupListSerializer',
    -    'UserGroupUpdateMemberSerializer',
    +    'UserGroupSerializer',
     ]
     
     
     class UserGroupSerializer(BulkOrgResourceModelSerializer):
         users = serializers.PrimaryKeyRelatedField(
    -        required=False, many=True, queryset=User.objects, label=_('User')
    +        required=False, many=True, queryset=User.objects, label=_('User'),
    +        write_only=True
         )
     
         class Meta:
             model = UserGroup
             list_serializer_class = AdaptedBulkListSerializer
             fields = [
    -            'id', 'name',  'users', 'comment', 'date_created',
    -            'created_by',
    +            'id', 'name',  'users', 'users_amount', 'comment',
    +            'date_created', 'created_by',
             ]
             extra_kwargs = {
                 'created_by': {'label': _('Created by'), 'read_only': True}
    @@ -47,23 +47,8 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer):
                     raise serializers.ValidationError(msg)
             return users
     
    -
    -class UserGroupListSerializer(UserGroupSerializer):
    -    users = StringManyToManyField(many=True, read_only=True)
    -
    -
    -class UserGroupUpdateMemberSerializer(serializers.ModelSerializer):
    -    users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects)
    -
    -    class Meta:
    -        model = UserGroup
    -        fields = ['id', 'users']
    -
    -    def __init__(self, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    -        self.set_fields_queryset()
    -
    -    def set_fields_queryset(self):
    -        users_field = self.fields['users']
    -        users_field.child_relation.queryset = utils.get_current_org_members()
    -
    +    @classmethod
    +    def setup_eager_loading(cls, queryset):
    +        """ Perform necessary eager loading of data. """
    +        queryset = queryset.annotate(users_amount=Count('users'))
    +        return queryset
    diff --git a/apps/users/serializers/realtion.py b/apps/users/serializers/realtion.py
    index 15cc9e43c..b768c57fc 100644
    --- a/apps/users/serializers/realtion.py
    +++ b/apps/users/serializers/realtion.py
    @@ -8,9 +8,11 @@ __all__ = ['UserUserGroupRelationSerializer']
     
     
     class UserUserGroupRelationSerializer(serializers.ModelSerializer):
    -    user_name = serializers.CharField(read_only=True)
    -    usergroup_name = serializers.CharField(read_only=True)
    +    user_display = serializers.CharField(read_only=True)
    +    usergroup_display = serializers.CharField(read_only=True)
     
         class Meta:
             model = User.groups.through
    -        fields = ['id', 'user', 'user_name', 'usergroup', 'usergroup_name']
    +        fields = [
    +            'id', 'user', 'user_display', 'usergroup', 'usergroup_display'
    +        ]
    diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py
    index 256dc073d..2363c39ed 100644
    --- a/apps/users/serializers/user.py
    +++ b/apps/users/serializers/user.py
    @@ -7,11 +7,11 @@ from common.utils import validate_ssh_public_key
     from common.mixins import BulkSerializerMixin
     from common.serializers import AdaptedBulkListSerializer
     from common.permissions import CanUpdateDeleteUser
    -from ..models import User, UserGroup
    +from ..models import User
     
     
     __all__ = [
    -    'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer',
    +    'UserSerializer', 'UserPKUpdateSerializer',
         'ChangeUserPasswordSerializer', 'ResetOTPSerializer',
         'UserProfileSerializer', 'UserDisplaySerializer',
     ]
    @@ -123,16 +123,6 @@ class UserPKUpdateSerializer(serializers.ModelSerializer):
             return value
     
     
    -class UserUpdateGroupSerializer(serializers.ModelSerializer):
    -    groups = serializers.PrimaryKeyRelatedField(
    -        many=True, queryset=UserGroup.objects
    -    )
    -
    -    class Meta:
    -        model = User
    -        fields = ['id', 'groups']
    -
    -
     class ChangeUserPasswordSerializer(serializers.ModelSerializer):
     
         class Meta:
    diff --git a/apps/users/signals.py b/apps/users/signals.py
    index b9084b7dc..37969f839 100644
    --- a/apps/users/signals.py
    +++ b/apps/users/signals.py
    @@ -2,3 +2,4 @@ from django.dispatch import Signal
     
     
     post_user_create = Signal(providing_args=('user',))
    +post_user_change_password = Signal(providing_args=('user',))
    diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py
    index a33d9eec9..902dfa4d7 100644
    --- a/apps/users/signals_handler.py
    +++ b/apps/users/signals_handler.py
    @@ -2,7 +2,7 @@
     #
     
     from django.dispatch import receiver
    -from django.db.models.signals import post_save, m2m_changed
    +from django.db.models.signals import m2m_changed
     
     from common.utils import get_logger
     from .signals import post_user_create
    diff --git a/apps/users/templates/users/_base_otp.html b/apps/users/templates/users/_base_otp.html
    index ac511efb0..89ac8d656 100644
    --- a/apps/users/templates/users/_base_otp.html
    +++ b/apps/users/templates/users/_base_otp.html
    @@ -1,60 +1,20 @@
    +{% extends '_without_nav_base.html' %}
     {% load static %}
     {% load i18n %}
     
    -
    -
    -	
    -		
    -		 {{ JMS_TITLE }} 
    -        
    -		
    -		
    -        
    -        
    -        
    -	
    -
    -	
    -		
    -		 
    - - -
    - - -
    -
    -

    - {% block small_title %} - {% endblock %} -

    -
    -
    -
    {% trans 'Security token validation' %}  {% trans 'Account' %} {{ user.username }}  {% trans 'Follow these steps to complete the binding operation' %}
    -
    - {% block content %} +{% block body %} +
    +
    +

    + {% block small_title %} {% endblock %} -

    -
    - - -
    -
    - {% include '_copyright.html' %} -
    -
    - - - - + +
    +
    +
    {% trans 'Security token validation' %}  {% trans 'Account' %} {{ user.username }}  {% trans 'Follow these steps to complete the binding operation' %}
    +
    + {% block content %} + {% endblock %} +
    +
    +{% endblock %} diff --git a/apps/users/templates/users/_base_user_detail.html b/apps/users/templates/users/_base_user_detail.html new file mode 100644 index 000000000..936037ab8 --- /dev/null +++ b/apps/users/templates/users/_base_user_detail.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    +
    + +
    +
    + {% block content_table %} + {% endblock %} +
    +
    +
    +
    +
    +{% endblock %} diff --git a/apps/users/templates/users/_user_detail_nav_header.html b/apps/users/templates/users/_user_detail_nav_header.html new file mode 100644 index 000000000..28079e149 --- /dev/null +++ b/apps/users/templates/users/_user_detail_nav_header.html @@ -0,0 +1,97 @@ +{% load static %} +{% load i18n %} + + + +
  • + {% trans 'User detail' %} +
  • +
  • + + {% trans "User permissions" %} + + + + +
  • + + \ No newline at end of file diff --git a/apps/users/templates/users/_user_groups_import_modal.html b/apps/users/templates/users/_user_groups_import_modal.html deleted file mode 100644 index 63d057215..000000000 --- a/apps/users/templates/users/_user_groups_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% 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 diff --git a/apps/users/templates/users/_user_groups_update_modal.html b/apps/users/templates/users/_user_groups_update_modal.html deleted file mode 100644 index a07c0f82c..000000000 --- a/apps/users/templates/users/_user_groups_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update user group" %}{% endblock %} \ No newline at end of file diff --git a/apps/users/templates/users/_user_import_modal.html b/apps/users/templates/users/_user_import_modal.html deleted file mode 100644 index e53d67fa7..000000000 --- a/apps/users/templates/users/_user_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import users" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-users:user-list" %}{% endblock %} diff --git a/apps/users/templates/users/_user_update_modal.html b/apps/users/templates/users/_user_update_modal.html deleted file mode 100644 index 9dfe60c96..000000000 --- a/apps/users/templates/users/_user_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update user" %}{% endblock %} \ No newline at end of file diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html index 1038fb8c8..fb8af6257 100644 --- a/apps/users/templates/users/first_login.html +++ b/apps/users/templates/users/first_login.html @@ -13,7 +13,7 @@ {% block content %}
    -
    +
    {% trans 'First Login' %}
    @@ -55,7 +55,7 @@
    -
    + {% csrf_token %} {{ wizard.management_form }} {#{% if wizard.form.forms %}#} @@ -88,7 +88,7 @@ {% endif %} -
    +
    diff --git a/apps/users/templates/users/first_login_done.html b/apps/users/templates/users/first_login_done.html index 5639d2c5d..ae43d032b 100644 --- a/apps/users/templates/users/first_login_done.html +++ b/apps/users/templates/users/first_login_done.html @@ -13,7 +13,7 @@ {% block content %}
    -
    +
    {% trans 'First Login' %}
    diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html index 3f3c7fabb..d48cf0277 100644 --- a/apps/users/templates/users/forgot_password.html +++ b/apps/users/templates/users/forgot_password.html @@ -1,60 +1,34 @@ +{% extends '_base_only_content.html' %} {% load static %} {% load i18n %} - - +{% load bootstrap3 %} +{% block custom_head_css_js %} + +{% endblock %} +{% block html_title %}{% trans 'Forgot password' %}{% endblock %} +{% block title %} {% trans 'Forgot password' %}?{% endblock %} - - - - - {% trans 'Forgot password' %} +{% block content %} + {% if errors %} +

    {{ errors }}

    + {% endif %} +

    + {% trans 'Input your email, that will send a mail to your' %} +

    - {% include '_head_css_js.html' %} - - - - - - -
    -
    - -
    -
    - -

    {% trans 'Forgot password' %} ?

    -

    - {% if errors %} -

    {{ errors }}

    - {% endif %} -

    - {% trans 'Input your email, that will send a mail to your' %} -

    - -
    -
    -
    - {% csrf_token %} -
    - -
    - - - - -
    -
    -
    -
    -
    -
    -
    -
    - {% include '_copyright.html' %} -
    +
    +
    +
    + {% csrf_token %} + {% bootstrap_field form.email layout="horizontal" %} + {% bootstrap_field form.captcha layout="horizontal" %} + +
    +{% endblock %} - - - diff --git a/apps/users/templates/users/reset_password.html b/apps/users/templates/users/reset_password.html index 6817f0a75..ecad676bf 100644 --- a/apps/users/templates/users/reset_password.html +++ b/apps/users/templates/users/reset_password.html @@ -1,136 +1,80 @@ +{% extends '_base_only_content.html' %} {% load static %} {% load i18n %} - - +{% load bootstrap3 %} +{% block html_title %}{% trans 'Reset password' %}{% endblock %} +{% block title %}{% trans 'Reset password' %}{% endblock %} - - - - {{ JMS_TITLE }} - - {% include '_head_css_js.html' %} - - - - - - - - - -
    -
    - -
    -

    {% trans 'Welcome to the Jumpserver open source fortress' %}

    - -

    - {% trans 'Jumpserver is an open source desktop system developed using Python and Django that helps Internet businesses with efficient users, assets, permissions, and audit management' %} -

    - -

    - {% trans 'We are from all over the world, we have great admiration and worship for the spirit of open source, we have endless pursuit for perfection, neatness and elegance' %} -

    - -

    - {% trans 'We focus on automatic operation and maintenance, and strive to build an easy-to-use, stable, safe and automatic board hopping machine, which is our unremitting pursuit and power' %} -

    - -

    - {% trans 'Always young, always with tears in my eyes. Stay foolish Stay hungry' %} -

    - -
    -
    -
    -
    {% trans 'Reset password' %}
    -
    - {% csrf_token %} - {% if errors %} -

    {{ errors }}

    - {% endif %} -
    - - {# 密码popover #} -
    - -
    -
    -
    - -
    - - - - Forgot password? - - -

    -

    - -

    -

    +{% block content %} +
    + {% csrf_token %} + {% if errors %} +

    {{ errors }}

    + {% endif %} + {% if not token_invalid %} +
    + {% bootstrap_field form.new_password %} + {% bootstrap_field form.confirm_password %} + {# 密码popover #} +
    +
    -
    -
    -
    - {% include '_copyright.html' %} -
    -
    -
    + + {% endif %} + +{% endblock %} - +{% block custom_foot_js %} + + +{% endblock %} diff --git a/apps/users/templates/users/user_asset_permission.html b/apps/users/templates/users/user_asset_permission.html new file mode 100644 index 000000000..21ae72722 --- /dev/null +++ b/apps/users/templates/users/user_asset_permission.html @@ -0,0 +1,201 @@ +{% extends 'users/_base_user_detail.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + +{% endblock %} + + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'Asset' %}{% trans 'Node' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +
    +
    +
    + +{% include '_filter_dropdown.html' %} + +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/users/templates/users/user_database_app_permission.html b/apps/users/templates/users/user_database_app_permission.html new file mode 100644 index 000000000..73b3a7972 --- /dev/null +++ b/apps/users/templates/users/user_database_app_permission.html @@ -0,0 +1,168 @@ +{% extends 'users/_base_user_detail.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + +{% endblock %} + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + +
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'DatabaseApp' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 2a6ed62b1..7bac7a454 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -1,345 +1,301 @@ -{% extends 'base.html' %} +{% extends 'users/_base_user_detail.html' %} {% load static %} {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} -{% block content %} -
    -
    -
    -
    - -
    -
    -
    -
    - {{ user_object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - - - - - - - {% if user.phone %} - - - - - {% endif %} - {% if user_object.wechat %} - - - - - {% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if user_object.can_update_password %} - - - - - {% endif %} - - - - - -
    - -
    {% trans 'Name' %}:{{ user_object.name }}
    {% trans 'Username' %}:{{ user_object.username }}
    {% trans 'Email' %}:{{ user_object.email }}
    {% trans 'Phone' %}:{{ user_object.phone }}
    {% trans 'Wechat' %}:{{ user_object.wechat }}
    {% trans 'Role' %}:{{ user_object.role_display }}
    {% trans 'MFA certification' %}: - {% if user_object.mfa_force_enabled %} - {% trans 'Force enabled' %} - {% elif user_object.mfa_enabled%} - {% trans 'Enabled' %} - {% else %} - {% trans 'Disabled' %} - {% endif %} -
    {% trans 'Source' %}:{{ user_object.get_source_display }}
    {% trans 'Date expired' %}:{{ user_object.date_expired|date:"Y-m-j H:i:s" }}
    {% trans 'Created by' %}:{{ user_object.created_by }}
    {% trans 'Date joined' %}:{{ user_object.date_joined|date:"Y-m-j H:i:s" }}
    {% trans 'Last login' %}:{{ user_object.last_login|date:"Y-m-j H:i:s" }}
    {% trans 'Last password updated' %}:{{ user_object.date_password_last_updated|date:"Y-m-j H:i:s" }}
    {% trans 'Comment' %}:{{ user_object.comment }}
    -
    -
    -
    -
    -
    -
    - {% trans 'Quick modify' %} -
    -
    - - - - - - - - - - - - - - - {% if user_object.can_update_password %} - - - - - {% endif %} - {% if user_object.can_update_ssh_key %} - - - - - {% endif %} - - - - - -
    {% trans 'Active' %}: -
    -
    - - -
    -
    -
    {% trans 'Force enabled MFA' %}: - -
    -
    - - -
    -
    -
    -
    {% trans 'Reset MFA' %}: - - - -
    {% trans 'Send reset password mail' %}: - - - -
    {% trans 'Send reset ssh key mail' %}: - - - -
    {% trans 'Unblock user' %} - - - -
    -
    -
    - {% if request.user.can_admin_current_org %} +{% block content_nav_delete_update %} +
  • + {% trans 'Update' %} +
  • +
  • + + {% trans 'Delete' %} + +
  • +{% endblock %} - {% if user_object.can_user_current_org or user_object.can_admin_current_org %} -
    -
    - {% trans 'User group' %} -
    -
    - - - - - - - - - - - - {% for group in user_object.groups.all %} - - - - - {% endfor %} - -
    - -
    - -
    - {{ group.name }} - - -
    -
    -
    - {% endif %} - - {% if LICENSE_VALID and LOGIN_CONFIRM_ENABLE %} -
    -
    - {% trans 'Login confirm' %} -
    -
    - - - - - - - - - - - {% if user_object.get_login_confirm_setting %} - {% for u in user_object.login_confirm_setting.reviewers.all %} - - - - - {% endfor %} - {% endif %} - -
    - -
    - -
    - {{ u }} - - -
    -
    -
    - {% endif %} - - {% endif %} -
    -
    +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    +
    + + + + + + + + + + + + + + + + + + {% if user.phone %} + + + + + {% endif %} + {% if object.wechat %} + + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if object.can_update_password %} + + + + + {% endif %} + + + + + +
    + +
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'Username' %}:{{ object.username }}
    {% trans 'Email' %}:{{ object.email }}
    {% trans 'Phone' %}:{{ object.phone }}
    {% trans 'Wechat' %}:{{ object.wechat }}
    {% trans 'Role' %}:{{ object.role_display }}
    {% trans 'MFA certification' %}: + {% if object.mfa_force_enabled %} + {% trans 'Force enabled' %} + {% elif object.mfa_enabled%} + {% trans 'Enabled' %} + {% else %} + {% trans 'Disabled' %} + {% endif %} +
    {% trans 'Source' %}:{{ object.get_source_display }}
    {% trans 'Date expired' %}:{{ object.date_expired|date:"Y-m-j H:i:s" }}
    {% trans 'Created by' %}:{{ object.created_by }}
    {% trans 'Date joined' %}:{{ object.date_joined|date:"Y-m-j H:i:s" }}
    {% trans 'Last login' %}:{{ object.last_login|date:"Y-m-j H:i:s" }}
    {% trans 'Last password updated' %}:{{ object.date_password_last_updated|date:"Y-m-j H:i:s" }}
    {% trans 'Comment' %}:{{ object.comment }}
    +
    - {% include 'users/_user_update_pk_modal.html' %} +
    +
    +
    + {% trans 'Quick modify' %} +
    +
    + + + + + + + + + + + + + + + {% if object.can_update_password %} + + + + + {% endif %} + {% if object.can_update_ssh_key %} + + + + + {% endif %} + + + + + +
    {% trans 'Active' %}: + +
    +
    + + +
    +
    +
    +
    {% trans 'Force enabled MFA' %}: + +
    +
    + + +
    +
    +
    +
    {% trans 'Reset MFA' %}: + + + +
    {% trans 'Send reset password mail' %}: + + + +
    {% trans 'Send reset ssh key mail' %}: + + + +
    {% trans 'Unblock user' %} + + + +
    +
    +
    + {% if request.user.can_admin_current_org %} + + {% if object.can_user_current_org or object.can_admin_current_org %} +
    +
    + {% trans 'User group' %} +
    +
    + + + + + + + + + + + + {% for group in object.groups.all %} + + + + + {% endfor %} + +
    + +
    + +
    + {{ group.name }} + + +
    +
    +
    + {% endif %} + + {% if LICENSE_VALID and LOGIN_CONFIRM_ENABLE %} +
    +
    + {% trans 'Login confirm' %} +
    +
    + + + + + + + + + + + {% if object.get_login_confirm_setting %} + {% for u in object.login_confirm_setting.reviewers.all %} + + + + + {% endfor %} + {% endif %} + +
    + +
    + +
    + {{ u }} + + +
    +
    +
    + {% endif %} + + {% endif %} +
    + +{% include 'users/_user_update_pk_modal.html' %} + {% endblock %} + {% block custom_foot_js %} {% endblock %} -{% block content %} -
    -
    -
    -
    - -
    - {% include 'users/_granted_assets.html' %} -
    -
    -
    -
    -
    + +{% block content_table %} +{% include 'users/_granted_assets.html' %} {% endblock %} + {% block custom_foot_js %} +{% endblock %} + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Database' %}{% trans 'Comment' %}
    +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/users/templates/users/user_granted_remote_app.html b/apps/users/templates/users/user_granted_remote_app.html new file mode 100644 index 000000000..19b8ab22c --- /dev/null +++ b/apps/users/templates/users/user_granted_remote_app.html @@ -0,0 +1,93 @@ +{% extends 'users/_base_user_detail.html' %} +{% load i18n static %} + +{% block custom_head_css_js %} + +{% endblock %} + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'App type' %}{% trans 'Asset' %}{% trans 'Comment' %}
    +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/users/templates/users/user_group_create_update.html b/apps/users/templates/users/user_group_create_update.html index 25f027ad5..09030eca8 100644 --- a/apps/users/templates/users/user_group_create_update.html +++ b/apps/users/templates/users/user_group_create_update.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    diff --git a/apps/users/templates/users/user_group_detail.html b/apps/users/templates/users/user_group_detail.html index 8f5cccf29..cd65bb746 100644 --- a/apps/users/templates/users/user_group_detail.html +++ b/apps/users/templates/users/user_group_detail.html @@ -3,13 +3,8 @@ {% load i18n %} {% block custom_head_css_js %} - - - - - {% endblock %} {% block content %}
    @@ -96,9 +91,9 @@ {% for user in user_group.users.all %} - {{ user.name }} + {{ user.name }} - + {% endfor %} @@ -115,73 +110,51 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html index 3eb70e319..5e354d3db 100644 --- a/apps/users/templates/users/user_group_list.html +++ b/apps/users/templates/users/user_group_list.html @@ -1,28 +1,7 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} @@ -39,14 +18,13 @@ -{% include "users/_user_groups_import_modal.html" %} -{% include "users/_user_groups_update_modal.html" %} {% endblock %} {% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index 265d4a4db..63dd981be 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -1,28 +1,7 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} @@ -47,7 +26,11 @@