From 4b7af1457d14e5779d47fb4b6b65dc5b9f382d24 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Thu, 17 May 2018 11:39:04 +0800
Subject: [PATCH 1/8] =?UTF-8?q?[Update]=20=E7=B3=BB=E7=BB=9F=E7=94=A8?=
 =?UTF-8?q?=E6=88=B7=E6=96=B0=E5=A2=9E=E6=B8=85=E9=99=A4=E8=AE=A4=E8=AF=81?=
 =?UTF-8?q?=E7=9A=84=E6=93=8D=E4=BD=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/assets/api/system_user.py                |   7 +-
 apps/assets/models/base.py                    |   1 +
 .../templates/assets/system_user_detail.html  |  25 +++++--
 apps/i18n/zh/LC_MESSAGES/django.mo            | Bin 33242 -> 33388 bytes
 apps/i18n/zh/LC_MESSAGES/django.po            |  61 +++++++++++-------
 apps/users/models/group.py                    |   2 +-
 6 files changed, 67 insertions(+), 29 deletions(-)

diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py
index 1be316567..66d62232d 100644
--- a/apps/assets/api/system_user.py
+++ b/apps/assets/api/system_user.py
@@ -40,7 +40,7 @@ class SystemUserViewSet(BulkModelViewSet):
     permission_classes = (IsSuperUserOrAppUser,)
 
 
-class SystemUserAuthInfoApi(generics.RetrieveUpdateAPIView):
+class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
     """
     Get system user auth info
     """
@@ -48,6 +48,11 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateAPIView):
     permission_classes = (IsSuperUserOrAppUser,)
     serializer_class = serializers.SystemUserAuthSerializer
 
+    def destroy(self, request, *args, **kwargs):
+        instance = self.get_object()
+        instance.clear_auth()
+        return Response(status=204)
+
 
 class SystemUserPushApi(generics.RetrieveAPIView):
     """
diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py
index 11afd92cf..cb9bb96ae 100644
--- a/apps/assets/models/base.py
+++ b/apps/assets/models/base.py
@@ -107,6 +107,7 @@ class AssetUser(models.Model):
     def clear_auth(self):
         self._password = ''
         self._private_key = ''
+        self._public_key = ''
         self.save()
 
     def auto_gen_auth(self):
diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html
index cc686a62b..9a7bc255a 100644
--- a/apps/assets/templates/assets/system_user_detail.html
+++ b/apps/assets/templates/assets/system_user_detail.html
@@ -107,14 +107,14 @@
                         </div>
 
                         <div class="col-sm-4" style="padding-left: 0;padding-right: 0">
-                            <div class="panel panel-primary only-ssh">
+                            <div class="panel panel-primary ">
                                 <div class="panel-heading">
                                     <i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
                                 </div>
                                 <div class="panel-body">
                                     <table class="table">
                                         <tbody>
-                                        <tr class="no-borders-tr">
+                                        <tr class="only-ssh">
                                             <td width="50%">{% trans 'Auto push' %}:</td>
                                             <td>
                                                 <span class="pull-right">
@@ -130,8 +130,8 @@
                                                 </span>
                                             </td>
                                         </tr>
-                                        <tr class="no-borders-tr">
                                         {% if system_user.auto_push %}
+                                        <tr class="only-ssh">
                                             <td width="50%">{% trans 'Push system user now' %}:</td>
                                             <td>
                                                 <span style="float: right">
@@ -139,8 +139,8 @@
                                                 </span>
                                             </td>
                                         </tr>
-                                        <tr>
                                         {% endif %}
+                                        <tr class="only-ssh">
                                             <td width="50%">{% trans 'Test assets connective' %}:</td>
                                             <td>
                                                 <span style="float: right">
@@ -149,6 +149,15 @@
                                             </td>
                                         </tr>
 
+                                        <tr>
+                                            <td width="50%">{% trans 'Clear auth' %}:</td>
+                                            <td>
+                                                <span style="float: right">
+                                                    <button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>
+                                                </span>
+                                            </td>
+                                        </tr>
+
 {#                                        <tr>#}
 {#                                            <td width="50%">{% trans 'Change auth period' %}:</td>#}
 {#                                            <td>#}
@@ -239,6 +248,7 @@ $(document).ready(function () {
     if($('#id_protocol_type').text() === 'rdp'){
         $('.only-ssh').addClass('hidden')
     }
+    $(".panel-body .table tr:visible:first").addClass('no-borders-tr');
     $('.select2').select2()
     .on('select2:select', function(evt) {
             var data = evt.params.data;
@@ -321,6 +331,13 @@ $(document).ready(function () {
         success: success,
         flash_message: false
     });
+}).on('click', '.btn-clear-auth', function () {
+    var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
+    APIUpdateAttr({
+        url: the_url,
+        method: 'DELETE',
+        success_message: "{% trans 'Clear auth' %}" + " {% trans 'success' %}"
+    });
 })
 </script>
 {% endblock %}
diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo
index 36220e3219e13e2f33a24be9281b1ef120ef808e..6b02da8fad05df4e52dd43f5833a98e2ab222cc0 100644
GIT binary patch
delta 11602
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
DXY(YG

delta 11471
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

diff --git a/apps/i18n/zh/LC_MESSAGES/django.po b/apps/i18n/zh/LC_MESSAGES/django.po
index 8f95f9291..196c3fb36 100644
--- a/apps/i18n/zh/LC_MESSAGES/django.po
+++ b/apps/i18n/zh/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Jumpserver 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-08 17:24+0800\n"
+"POT-Creation-Date: 2018-05-17 11:32+0800\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: Jumpserver team<ibuler@qq.com>\n"
@@ -17,22 +17,22 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: assets/api/node.py:96
+#: assets/api/node.py:106
 msgid "New node {}"
 msgstr "新节点 {}"
 
-#: assets/api/node.py:225
+#: assets/api/node.py:242
 msgid "更新节点资产硬件信息: {}"
 msgstr ""
 
-#: assets/api/node.py:238
+#: assets/api/node.py:255
 msgid "测试节点下资产是否可连接: {}"
 msgstr ""
 
 #: assets/forms/asset.py:24 assets/models/asset.py:66 assets/models/user.py:103
 #: assets/templates/assets/asset_detail.html:183
 #: assets/templates/assets/asset_detail.html:191
-#: assets/templates/assets/system_user_detail.html:166 perms/models.py:33
+#: assets/templates/assets/system_user_detail.html:175 perms/models.py:33
 msgid "Nodes"
 msgstr "节点管理"
 
@@ -438,7 +438,7 @@ msgstr "默认资产组"
 msgid "User"
 msgstr "用户"
 
-#: assets/models/label.py:18 assets/models/node.py:15
+#: assets/models/label.py:18 assets/models/node.py:18
 #: assets/templates/assets/label_list.html:15 common/models.py:27
 msgid "Value"
 msgstr "值"
@@ -535,7 +535,7 @@ msgstr "测试系统用户可连接性: {}"
 msgid "定期测试系统用户可连接性: {}"
 msgstr ""
 
-#: assets/tasks.py:401
+#: assets/tasks.py:402
 msgid "推送系统用户到入资产: {}"
 msgstr ""
 
@@ -660,7 +660,7 @@ msgstr "重置"
 #: common/templates/common/ldap_setting.html:60
 #: common/templates/common/terminal_setting.html:103
 #: perms/templates/perms/asset_permission_create_update.html:70
-#: terminal/templates/terminal/session_list.html:120
+#: terminal/templates/terminal/session_list.html:124
 #: terminal/templates/terminal/terminal_update.html:48
 #: users/templates/users/_user.html:47
 #: users/templates/users/forgot_password.html:44
@@ -782,8 +782,8 @@ msgstr "选择节点"
 
 #: assets/templates/assets/admin_user_detail.html:100
 #: assets/templates/assets/asset_detail.html:200
-#: assets/templates/assets/asset_list.html:634
-#: assets/templates/assets/system_user_detail.html:183
+#: assets/templates/assets/asset_list.html:636
+#: assets/templates/assets/system_user_detail.html:192
 #: assets/templates/assets/system_user_list.html:138 templates/_modal.html:22
 #: terminal/templates/terminal/session_detail.html:108
 #: users/templates/users/user_detail.html:362
@@ -963,19 +963,19 @@ msgstr "仅显示当前节点资产"
 msgid "Displays all child node assets"
 msgstr "显示所有子节点资产"
 
-#: assets/templates/assets/asset_list.html:215
+#: assets/templates/assets/asset_list.html:217
 msgid "Create node failed"
 msgstr "创建节点失败"
 
-#: assets/templates/assets/asset_list.html:227
+#: assets/templates/assets/asset_list.html:229
 msgid "Have child node, cancel"
 msgstr "存在子节点,不能删除"
 
-#: assets/templates/assets/asset_list.html:229
+#: assets/templates/assets/asset_list.html:231
 msgid "Have assets, cancel"
 msgstr "存在资产,不能删除"
 
-#: assets/templates/assets/asset_list.html:629
+#: assets/templates/assets/asset_list.html:631
 #: assets/templates/assets/system_user_list.html:133
 #: users/templates/users/user_detail.html:357
 #: users/templates/users/user_detail.html:382
@@ -984,20 +984,20 @@ msgstr "存在资产,不能删除"
 msgid "Are you sure?"
 msgstr "你确认吗?"
 
-#: assets/templates/assets/asset_list.html:630
+#: assets/templates/assets/asset_list.html:632
 msgid "This will delete the selected assets !!!"
 msgstr "删除选择资产"
 
-#: assets/templates/assets/asset_list.html:638
+#: assets/templates/assets/asset_list.html:640
 msgid "Asset Deleted."
 msgstr "已被删除"
 
-#: assets/templates/assets/asset_list.html:639
-#: assets/templates/assets/asset_list.html:644
+#: assets/templates/assets/asset_list.html:641
+#: assets/templates/assets/asset_list.html:646
 msgid "Asset Delete"
 msgstr "删除"
 
-#: assets/templates/assets/asset_list.html:643
+#: assets/templates/assets/asset_list.html:645
 msgid "Asset Deleting failed."
 msgstr "删除失败"
 
@@ -1032,6 +1032,7 @@ msgid "Create gateway"
 msgstr "创建网关"
 
 #: assets/templates/assets/domain_gateway_list.html:87
+#: assets/templates/assets/domain_gateway_list.html:89
 #: common/templates/common/email_setting.html:58
 #: common/templates/common/ldap_setting.html:58
 msgid "Test connection"
@@ -1080,10 +1081,23 @@ msgstr "家目录"
 msgid "Uid"
 msgstr "Uid"
 
-#: assets/templates/assets/system_user_detail.html:174
+#: assets/templates/assets/system_user_detail.html:153
+#: assets/templates/assets/system_user_detail.html:339
+msgid "Clear auth"
+msgstr "清除认证信息"
+
+#: assets/templates/assets/system_user_detail.html:156
+msgid "Clear"
+msgstr "清除"
+
+#: assets/templates/assets/system_user_detail.html:183
 msgid "Add to node"
 msgstr "添加到节点"
 
+#: assets/templates/assets/system_user_detail.html:339
+msgid "success"
+msgstr "成功"
+
 #: assets/templates/assets/system_user_list.html:18
 #: assets/views/system_user.py:45
 msgid "Create system user"
@@ -2113,15 +2127,16 @@ msgstr "时长"
 msgid "Monitor"
 msgstr "监控"
 
-#: terminal/templates/terminal/session_list.html:105
+#: terminal/templates/terminal/session_list.html:106
+#: terminal/templates/terminal/session_list.html:108
 msgid "Terminate"
 msgstr "终断"
 
-#: terminal/templates/terminal/session_list.html:116
+#: terminal/templates/terminal/session_list.html:120
 msgid "Terminate selected"
 msgstr "终断所选"
 
-#: terminal/templates/terminal/session_list.html:136
+#: terminal/templates/terminal/session_list.html:140
 msgid "Terminate task send, waiting ..."
 msgstr "终断任务已发送,请等待"
 
diff --git a/apps/users/models/group.py b/apps/users/models/group.py
index 128fbdbcb..48ed2e949 100644
--- a/apps/users/models/group.py
+++ b/apps/users/models/group.py
@@ -11,7 +11,7 @@ __all__ = ['UserGroup']
 
 class UserGroup(NoDeleteModelMixin):
     id = models.UUIDField(default=uuid.uuid4, primary_key=True)
-    name = models.CharField(max_length=128, verbose_name=_('Name'))
+    name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
     comment = models.TextField(blank=True, verbose_name=_('Comment'))
     date_created = models.DateTimeField(auto_now_add=True, null=True,
                                         verbose_name=_('Date created'))

From 46520287d940daee4444e1a217adf609fe4bc7e6 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Thu, 17 May 2018 15:26:22 +0800
Subject: [PATCH 2/8] =?UTF-8?q?[Update]=20=E6=B7=BB=E5=8A=A0=E6=B8=85?=
 =?UTF-8?q?=E7=90=86=E9=87=8D=E5=A4=8D=E7=94=A8=E6=88=B7=E7=BB=84=E8=84=9A?=
 =?UTF-8?q?=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 utils/clean_duplicate_user_groups.py | 70 ++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)
 create mode 100644 utils/clean_duplicate_user_groups.py

diff --git a/utils/clean_duplicate_user_groups.py b/utils/clean_duplicate_user_groups.py
new file mode 100644
index 000000000..be8b2d9ec
--- /dev/null
+++ b/utils/clean_duplicate_user_groups.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+#
+
+import os
+import sys
+from collections import Counter
+import django
+from django.db.models import Count
+
+
+if os.path.exists('../apps'):
+    sys.path.insert(0, '../apps')
+elif os.path.exists('./apps'):
+    sys.path.insert(0, './apps')
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
+django.setup()
+
+from users.models import UserGroup
+
+
+def clean_group(interactive=True):
+    groups = UserGroup.objects.all()
+    groups_name_list = groups.values_list('name', flat=True)
+    groups_with_info = groups.annotate(Count('users'))\
+        .annotate(Count('asset_permissions'))
+
+    counter = Counter(groups_name_list)
+    for name, count in counter.items():
+        if count == 0:
+            continue
+        groups_duplicate = groups_with_info.filter(name=name)
+        need_clean_count = groups_duplicate.count()
+
+        for group in groups_duplicate:
+            need_clean = True
+            if group.users__count > 0:
+                need_clean = False
+            elif group.asset_permissions__count > 0:
+                need_clean = False
+            elif need_clean_count == 1:
+                need_clean = False
+
+            if need_clean:
+                confirm = True
+                if interactive:
+                    confirm = False
+                    while True:
+                        confirm = input(
+                            "Delete user group <{}>, create at {}? ([y]/n)".format(
+                                name, group.date_created)
+                        )
+                        if confirm.lower() == "y":
+                            confirm = True
+                            break
+                        elif confirm.lower() == "n":
+                            confirm = False
+                            break
+                        else:
+                            print("No valid input")
+                            continue
+                if confirm:
+                    group.delete()
+                    print("Delete success: {}".format(name))
+                    need_clean_count -= 1
+                else:
+                    continue
+
+if __name__ == '__main__':
+    clean_group()

From d615eb80b5672948a59aaa19d92095c5a5912a9c Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Tue, 22 May 2018 18:22:06 +0800
Subject: [PATCH 3/8] =?UTF-8?q?[Update]=20=E4=BC=98=E5=8C=96=E4=BD=BF?=
 =?UTF-8?q?=E7=94=A8storage=20sdk?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/terminal/api.py                        | 54 +++++++--------------
 apps/terminal/backends/__init__.py          | 16 +++---
 apps/terminal/backends/command/es.py        | 31 +++---------
 apps/terminal/serializers.py                |  4 +-
 apps/terminal/templatetags/terminal_tags.py |  4 +-
 apps/terminal/views/command.py              |  4 +-
 apps/terminal/views/session.py              |  4 +-
 requirements/requirements.txt               |  3 +-
 8 files changed, 41 insertions(+), 79 deletions(-)

diff --git a/apps/terminal/api.py b/apps/terminal/api.py
index c9bb68ed6..924a30dfd 100644
--- a/apps/terminal/api.py
+++ b/apps/terminal/api.py
@@ -9,6 +9,7 @@ from django.core.cache import cache
 from django.shortcuts import get_object_or_404, redirect
 from django.utils import timezone
 from django.core.files.storage import default_storage
+from django.http.response import HttpResponseRedirectBase
 from django.http import HttpResponseNotFound
 from django.conf import settings
 
@@ -25,7 +26,7 @@ from .serializers import TerminalSerializer, StatusSerializer, \
     SessionSerializer, TaskSerializer, ReplaySerializer
 from .hands import IsSuperUserOrAppUser, IsAppUser, \
     IsSuperUserOrAppUserOrUserReadonly
-from .backends import get_command_store, get_multi_command_store, \
+from .backends import get_command_storage, get_multi_command_storage, \
     SessionCommandSerializer
 
 logger = logging.getLogger(__file__)
@@ -227,8 +228,8 @@ class CommandViewSet(viewsets.ViewSet):
     }
 
     """
-    command_store = get_command_store()
-    multi_command_storage = get_multi_command_store()
+    command_store = get_command_storage()
+    multi_command_storage = get_multi_command_storage()
     serializer_class = SessionCommandSerializer
     permission_classes = (IsSuperUserOrAppUser,)
 
@@ -291,19 +292,20 @@ class SessionReplayViewSet(viewsets.ViewSet):
             url = default_storage.url(path)
             return redirect(url)
         else:
-            configs = settings.TERMINAL_REPLAY_STORAGE.items()
+            configs = settings.TERMINAL_REPLAY_STORAGE
+            configs = [cfg for cfg in configs if cfg['TYPE'] != 'server']
             if not configs:
                 return HttpResponseNotFound()
 
-            for name, config in configs:
-                client = jms_storage.init(config)
-                date = self.session.date_start.strftime('%Y-%m-%d')
-                file_path = os.path.join(date, str(self.session.id) + '.replay.gz')
-                target_path = default_storage.base_location + '/' + path
-
-                if client and client.has_file(file_path) and \
-                        client.download_file(file_path, target_path):
-                    return redirect(default_storage.url(path))
+            date = self.session.date_start.strftime('%Y-%m-%d')
+            file_path = os.path.join(date, str(self.session.id) + '.replay.gz')
+            target_path = default_storage.base_location + '/' + path
+            storage = jms_storage.get_multi_object_storage(configs)
+            ok, err = storage.download(file_path, target_path)
+            if ok:
+                return redirect(default_storage.url(path))
+            else:
+                logger.error("Failed download replay file: {}".format(err))
         return HttpResponseNotFound()
 
 
@@ -313,34 +315,14 @@ class SessionReplayV2ViewSet(SessionReplayViewSet):
     session = None
 
     def retrieve(self, request, *args, **kwargs):
-        session_id = kwargs.get('pk')
-        self.session = get_object_or_404(Session, id=session_id)
-        path = self.gen_session_path()
+        response = super().retrieve(request, *args, **kwargs)
         data = {
             'type': 'guacamole' if self.session.protocol == 'rdp' else 'json',
             'src': '',
         }
-
-        if default_storage.exists(path):
-            url = default_storage.url(path)
-            data['src'] = url
+        if isinstance(response, HttpResponseRedirectBase):
+            data['src'] = response.url
             return Response(data)
-        else:
-            configs = settings.TERMINAL_REPLAY_STORAGE.items()
-            if not configs:
-                return HttpResponseNotFound()
-
-            for name, config in configs:
-                client = jms_storage.init(config)
-                date = self.session.date_start.strftime('%Y-%m-%d')
-                file_path = os.path.join(date, str(self.session.id) + '.replay.gz')
-                target_path = default_storage.base_location + '/' + path
-
-                if client and client.has_file(file_path) and \
-                        client.download_file(file_path, target_path):
-                    url = default_storage.url(path)
-                    data['src'] = url
-                    return Response(data)
         return HttpResponseNotFound()
 
 
diff --git a/apps/terminal/backends/__init__.py b/apps/terminal/backends/__init__.py
index ef1ba56a9..9a1c338f5 100644
--- a/apps/terminal/backends/__init__.py
+++ b/apps/terminal/backends/__init__.py
@@ -7,19 +7,19 @@ TYPE_ENGINE_MAPPING = {
 }
 
 
-def get_command_store():
-    params = settings.COMMAND_STORAGE
-    engine_class = import_module(params['ENGINE'])
-    storage = engine_class.CommandStore(params)
+def get_command_storage():
+    config = settings.COMMAND_STORAGE
+    engine_class = import_module(config['ENGINE'])
+    storage = engine_class.CommandStore(config)
     return storage
 
 
-def get_terminal_command_store():
+def get_terminal_command_storages():
     storage_list = {}
     for name, params in settings.TERMINAL_COMMAND_STORAGE.items():
         tp = params['TYPE']
         if tp == 'server':
-            storage = get_command_store()
+            storage = get_command_storage()
         else:
             if not TYPE_ENGINE_MAPPING.get(tp):
                 continue
@@ -29,9 +29,9 @@ def get_terminal_command_store():
     return storage_list
 
 
-def get_multi_command_store():
+def get_multi_command_storage():
     from .command.multi import CommandStore
-    storage_list = get_terminal_command_store().values()
+    storage_list = get_terminal_command_storages().values()
     storage = CommandStore(storage_list)
     return storage
 
diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py
index 9c75fc978..dde8e1e95 100644
--- a/apps/terminal/backends/command/es.py
+++ b/apps/terminal/backends/command/es.py
@@ -1,41 +1,22 @@
 # -*- coding: utf-8 -*-
 #
 
-from jms_es_sdk import ESStore
+from jms_storage.es import ESStorage
 from .base import CommandBase
 from .models import AbstractSessionCommand
 
 
-class CommandStore(CommandBase, ESStore):
+class CommandStore(ESStorage, CommandBase):
     def __init__(self, params):
-        hosts = params.get('HOSTS', ['http://localhost'])
-        ESStore.__init__(self, hosts=hosts)
-
-    def save(self, command):
-        return ESStore.save(self, command)
-
-    def bulk_save(self, commands):
-        return ESStore.bulk_save(self, commands)
+        super().__init__(params)
 
     def filter(self, date_from=None, date_to=None,
                user=None, asset=None, system_user=None,
                input=None, session=None):
 
-        data = ESStore.filter(
-            self, date_from=date_from, date_to=date_to,
-            user=user, asset=asset, system_user=system_user,
-            input=input, session=session
-        )
+        data = super().filter(date_from=date_from, date_to=date_to,
+                              user=user, asset=asset, system_user=system_user,
+                              input=input, session=session)
         return AbstractSessionCommand.from_multi_dict(
             [item["_source"] for item in data["hits"] if item]
         )
-
-    def count(self, date_from=None, date_to=None,
-              user=None, asset=None, system_user=None,
-              input=None, session=None):
-        amount = ESStore.count(
-            self, date_from=date_from, date_to=date_to,
-            user=user, asset=asset, system_user=system_user,
-            input=input, session=session
-        )
-        return amount
diff --git a/apps/terminal/serializers.py b/apps/terminal/serializers.py
index 8c40315a7..c18c5023d 100644
--- a/apps/terminal/serializers.py
+++ b/apps/terminal/serializers.py
@@ -9,7 +9,7 @@ from rest_framework_bulk.serializers import BulkListSerializer
 from common.mixins import BulkSerializerMixin
 from common.utils import get_object_or_none
 from .models import Terminal, Status, Session, Task
-from .backends import get_multi_command_store
+from .backends import get_multi_command_storage
 
 
 class TerminalSerializer(serializers.ModelSerializer):
@@ -47,7 +47,7 @@ class TerminalSerializer(serializers.ModelSerializer):
 
 class SessionSerializer(serializers.ModelSerializer):
     command_amount = serializers.SerializerMethodField()
-    command_store = get_multi_command_store()
+    command_store = get_multi_command_storage()
 
     class Meta:
         model = Session
diff --git a/apps/terminal/templatetags/terminal_tags.py b/apps/terminal/templatetags/terminal_tags.py
index cd7120fec..c5643c67b 100644
--- a/apps/terminal/templatetags/terminal_tags.py
+++ b/apps/terminal/templatetags/terminal_tags.py
@@ -1,10 +1,10 @@
 # ~*~ coding: utf-8 ~*~
 
 from django import template
-from ..backends import get_multi_command_store
+from ..backends import get_multi_command_storage
 
 register = template.Library()
-command_store = get_multi_command_store()
+command_store = get_multi_command_storage()
 
 
 @register.filter
diff --git a/apps/terminal/views/command.py b/apps/terminal/views/command.py
index 0af0b5bfd..748261414 100644
--- a/apps/terminal/views/command.py
+++ b/apps/terminal/views/command.py
@@ -9,10 +9,10 @@ from django.utils.translation import ugettext as _
 from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
 from ..models import Command
 from .. import utils
-from ..backends import get_multi_command_store
+from ..backends import get_multi_command_storage
 
 __all__ = ['CommandListView']
-common_storage = get_multi_command_store()
+common_storage = get_multi_command_storage()
 
 
 class CommandListView(DatetimeSearchMixin, AdminUserRequiredMixin, ListView):
diff --git a/apps/terminal/views/session.py b/apps/terminal/views/session.py
index 3b66baff7..71caeae48 100644
--- a/apps/terminal/views/session.py
+++ b/apps/terminal/views/session.py
@@ -10,7 +10,7 @@ from django.conf import settings
 from users.utils import AdminUserRequiredMixin
 from common.mixins import DatetimeSearchMixin
 from ..models import Session, Command, Terminal
-from ..backends import get_multi_command_store
+from ..backends import get_multi_command_storage
 from .. import utils
 
 
@@ -19,7 +19,7 @@ __all__ = [
     'SessionDetailView',
 ]
 
-command_store = get_multi_command_store()
+command_store = get_multi_command_storage()
 
 
 class SessionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index e0ecc634e..5fb844181 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -40,7 +40,6 @@ itsdangerous==0.24
 itypes==1.1.0
 Jinja2==2.10
 jmespath==0.9.3
-jms-es-sdk
 kombu==4.0.2
 ldap3==2.4
 MarkupSafe==1.0
@@ -62,7 +61,7 @@ pytz==2017.3
 PyYAML==3.12
 redis==2.10.6
 requests==2.18.4
-jms-storage==0.0.13
+jms-storage==0.0.15
 s3transfer==0.1.13
 simplejson==3.13.2
 six==1.11.0

From 092b33d4d1ca10a3dbe7c9c93b8db467796cdce2 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Tue, 22 May 2018 19:30:15 +0800
Subject: [PATCH 4/8] =?UTF-8?q?[Update]=20=E6=9B=B4=E6=96=B0requirements?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 requirements/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 5fb844181..f3905f018 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -61,7 +61,7 @@ pytz==2017.3
 PyYAML==3.12
 redis==2.10.6
 requests==2.18.4
-jms-storage==0.0.15
+jms-storage==0.0.16
 s3transfer==0.1.13
 simplejson==3.13.2
 six==1.11.0

From dc1d228e07e5b17022044f4bb0984372c954761d Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Tue, 22 May 2018 19:44:13 +0800
Subject: [PATCH 5/8] =?UTF-8?q?[Update]=20=E6=9B=B4=E6=96=B0requirements?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 requirements/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index f3905f018..af682f5ca 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -61,7 +61,7 @@ pytz==2017.3
 PyYAML==3.12
 redis==2.10.6
 requests==2.18.4
-jms-storage==0.0.16
+jms-storage==0.0.17
 s3transfer==0.1.13
 simplejson==3.13.2
 six==1.11.0

From f8384973a1be85cac0df6e63630f1ec15a6be386 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Wed, 23 May 2018 15:15:27 +0800
Subject: [PATCH 6/8] =?UTF-8?q?[Update]=20=E8=B0=83=E6=95=B4Luna=E7=9A=84?=
 =?UTF-8?q?=E6=A0=91=E5=BD=A2=E7=BB=93=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/assets/models/node.py |  8 ++++++--
 apps/perms/api.py          |  9 +++++++--
 apps/perms/utils.py        | 34 +++++++++++++++++++++++++++++++---
 3 files changed, 44 insertions(+), 7 deletions(-)

diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py
index 5ee97af9b..fed8980ed 100644
--- a/apps/assets/models/node.py
+++ b/apps/assets/models/node.py
@@ -61,10 +61,14 @@ class Node(models.Model):
         return child
 
     def get_children(self):
-        return self.__class__.objects.filter(key__regex=r'^{}:[0-9]+$'.format(self.key))
+        return self.__class__.objects.filter(
+            key__regex=r'^{}:[0-9]+$'.format(self.key)
+        )
 
     def get_all_children(self):
-        return self.__class__.objects.filter(key__startswith='{}:'.format(self.key))
+        return self.__class__.objects.filter(
+            key__startswith='{}:'.format(self.key)
+        )
 
     def get_family(self):
         children = list(self.get_all_children())
diff --git a/apps/perms/api.py b/apps/perms/api.py
index be69d6158..8f663a0f4 100644
--- a/apps/perms/api.py
+++ b/apps/perms/api.py
@@ -6,7 +6,7 @@ from rest_framework.views import APIView, Response
 from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView
 from rest_framework import viewsets
 
-from common.utils import set_or_append_attr_bulk
+from common.utils import set_or_append_attr_bulk, get_object_or_none
 from users.permissions import IsValidUser, IsSuperUser, IsSuperUserOrAppUser
 from .utils import AssetPermissionUtil
 from .models import AssetPermission
@@ -147,8 +147,13 @@ class UserGrantedNodeAssetsApi(ListAPIView):
             user = get_object_or_404(User, id=user_id)
         else:
             user = self.request.user
-        node = get_object_or_404(Node, id=node_id)
         nodes = AssetPermissionUtil.get_user_nodes_with_assets(user)
+        node = get_object_or_none(Node, id=node_id)
+
+        if not node:
+            unnode = [node for node in nodes if node.name == 'Unnode']
+            node = unnode[0] if unnode else None
+
         assets = nodes.get(node, [])
         for asset, system_users in assets.items():
             asset.system_users_granted = system_users
diff --git a/apps/perms/utils.py b/apps/perms/utils.py
index b23b1cb7c..abcd2e17e 100644
--- a/apps/perms/utils.py
+++ b/apps/perms/utils.py
@@ -13,7 +13,6 @@ logger = get_logger(__file__)
 
 
 class AssetPermissionUtil:
-
     @staticmethod
     def get_user_permissions(user):
         return AssetPermission.objects.all().valid().filter(users=user)
@@ -122,6 +121,24 @@ class AssetPermissionUtil:
                 nodes[node].update(set(_system_users))
         return nodes
 
+    @classmethod
+    def get_user_nodes_inherit_group(cls, user):
+        nodes = defaultdict(set)
+        groups = user.groups.all()
+        for group in groups:
+            _nodes = cls.get_user_group_nodes(group)
+            for node, system_users in _nodes.items():
+                nodes[node].update(set(system_users))
+        return nodes
+
+    @classmethod
+    def get_user_nodes(cls, user):
+        nodes = cls.get_user_nodes_direct(user)
+        nodes_inherit = cls.get_user_nodes_inherit_group(user)
+        for node, system_users in nodes_inherit.items():
+            nodes[node].update(set(system_users))
+        return nodes
+
     @classmethod
     def get_user_nodes_assets_direct(cls, user):
         assets = defaultdict(set)
@@ -164,15 +181,26 @@ class AssetPermissionUtil:
         :param user:
         :return: {node: {asset: set(su1, su2)}}
         """
+        from assets.models import Node
+        unnode = Node(value='Unnode')
         nodes = defaultdict(dict)
+        for _node in cls.get_user_nodes(user):
+            children = _node.get_family()
+            for node in children:
+                nodes[node] = defaultdict(set)
         _assets = cls.get_user_assets(user)
         for asset, _system_users in _assets.items():
             _nodes = asset.get_nodes()
+            in_node = False
             for node in _nodes:
-                if asset in nodes[node]:
+                if node in nodes:
+                    in_node = True
                     nodes[node][asset].update(_system_users)
+            if not in_node:
+                if unnode in nodes:
+                    nodes[unnode][asset].update(_system_users)
                 else:
-                    nodes[node][asset] = _system_users
+                    nodes[unnode][asset] = _system_users
         return nodes
 
     @classmethod

From fe52c57a1133976c585d9db0acf0331c142a8c6e Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Thu, 24 May 2018 11:13:22 +0800
Subject: [PATCH 7/8] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E6=8E=88?=
 =?UTF-8?q?=E6=9D=83uitls=E7=9A=84=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/perms/utils.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/apps/perms/utils.py b/apps/perms/utils.py
index abcd2e17e..36349b982 100644
--- a/apps/perms/utils.py
+++ b/apps/perms/utils.py
@@ -197,10 +197,8 @@ class AssetPermissionUtil:
                     in_node = True
                     nodes[node][asset].update(_system_users)
             if not in_node:
-                if unnode in nodes:
-                    nodes[unnode][asset].update(_system_users)
-                else:
-                    nodes[unnode][asset] = _system_users
+                nodes[unnode] = defaultdict(set)
+                nodes[unnode][asset].update(_system_users)
         return nodes
 
     @classmethod

From c529061ee0ade5494d4f1e0947f8e12c3ef41792 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Thu, 24 May 2018 11:18:20 +0800
Subject: [PATCH 8/8] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E6=8E=88?=
 =?UTF-8?q?=E6=9D=83uitls=E7=9A=84=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/perms/utils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/perms/utils.py b/apps/perms/utils.py
index 36349b982..7899cc5cb 100644
--- a/apps/perms/utils.py
+++ b/apps/perms/utils.py
@@ -188,6 +188,7 @@ class AssetPermissionUtil:
             children = _node.get_family()
             for node in children:
                 nodes[node] = defaultdict(set)
+        nodes[unnode] = defaultdict(set)
         _assets = cls.get_user_assets(user)
         for asset, _system_users in _assets.items():
             _nodes = asset.get_nodes()
@@ -197,7 +198,6 @@ class AssetPermissionUtil:
                     in_node = True
                     nodes[node][asset].update(_system_users)
             if not in_node:
-                nodes[unnode] = defaultdict(set)
                 nodes[unnode][asset].update(_system_users)
         return nodes