diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py
index 51f9a8739..2478303d0 100644
--- a/apps/assets/api/node.py
+++ b/apps/assets/api/node.py
@@ -131,7 +131,7 @@ class NodeChildrenAsTreeApi(generics.ListAPIView):
         if not include_assets:
             return queryset
         assets = self.node.get_assets().only(
-            "id", "hostname", "ip", 'platform', "os", "org_id",
+            "id", "hostname", "ip", 'platform', "os", "org_id", "protocols",
         )
         for asset in assets:
             queryset.append(asset.as_tree_node(self.node))
diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py
index a37aa16b1..2161113e2 100644
--- a/apps/assets/models/asset.py
+++ b/apps/assets/models/asset.py
@@ -6,7 +6,8 @@ import uuid
 import logging
 import random
 from functools import reduce
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
+from django.core.cache import cache
 
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
@@ -96,7 +97,53 @@ class ProtocolsMixin:
         return self.protocols_as_dict.get("ssh", 22)
 
 
-class Asset(ProtocolsMixin, OrgModelMixin):
+class NodesRelationMixin:
+    NODES_CACHE_KEY = 'ASSET_NODES_{}'
+    ALL_ASSET_NODES_CACHE_KEY = 'ALL_ASSETS_NODES'
+    CACHE_TIME = 3600 * 24 * 7
+    id = ""
+    _all_nodes_keys = None
+
+    @classmethod
+    def get_all_nodes_keys(cls):
+        """
+        :return: {asset.id: [node.key, ]}
+        """
+        from .node import Node
+        cache_key = cls.ALL_ASSET_NODES_CACHE_KEY
+        cached = cache.get(cache_key)
+        if cached:
+            return cached
+        assets = Asset.objects.all().only('id').prefetch_related(
+            models.Prefetch('nodes', queryset=Node.objects.all().only('key'))
+        )
+        assets_nodes_keys = {}
+        for asset in assets:
+            assets_nodes_keys[asset.id] = [n.key for n in asset.nodes.all()]
+        cache.set(cache_key, assets_nodes_keys, cls.CACHE_TIME)
+        return assets_nodes_keys
+
+    @classmethod
+    def expire_all_nodes_keys_cache(cls):
+        cache_key = cls.ALL_ASSET_NODES_CACHE_KEY
+        cache.delete(cache_key)
+
+    def get_nodes(self):
+        from .node import Node
+        nodes = self.nodes.all() or [Node.root()]
+        return nodes
+
+    def get_all_nodes(self, flat=False):
+        nodes = []
+        for node in self.get_nodes():
+            _nodes = node.get_ancestor(with_self=True)
+            nodes.append(_nodes)
+        if flat:
+            nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
+        return nodes
+
+
+class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
     # Important
     PLATFORM_CHOICES = (
         ('Linux', 'Linux'),
@@ -182,20 +229,6 @@ class Asset(ProtocolsMixin, OrgModelMixin):
     def is_support_ansible(self):
         return self.has_protocol('ssh') and self.platform not in ("Other",)
 
-    def get_nodes(self):
-        from .node import Node
-        nodes = self.nodes.all() or [Node.root()]
-        return nodes
-
-    def get_all_nodes(self, flat=False):
-        nodes = []
-        for node in self.get_nodes():
-            _nodes = node.get_ancestor(with_self=True)
-            nodes.append(_nodes)
-        if flat:
-            nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
-        return nodes
-
     @property
     def cpu_info(self):
         info = ""
diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py
index aab5ebaf4..d668cea51 100644
--- a/apps/assets/models/node.py
+++ b/apps/assets/models/node.py
@@ -212,14 +212,12 @@ class AssetsAmountMixin:
         if cached is not None:
             return cached
         assets_amount = self.get_all_assets().count()
-        self.assets_amount = assets_amount
+        cache.set(cache_key, assets_amount, self.cache_time)
         return assets_amount
 
     @assets_amount.setter
     def assets_amount(self, value):
         self._assets_amount = value
-        cache_key = self._assets_amount_cache_key.format(self.key)
-        cache.set(cache_key, value, self.cache_time)
 
     def expire_assets_amount(self):
         ancestor_keys = self.get_ancestor_keys(with_self=True)
diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py
index 32f569b80..bbd808b80 100644
--- a/apps/assets/models/user.py
+++ b/apps/assets/models/user.py
@@ -117,16 +117,6 @@ class SystemUser(AssetUser):
     def __str__(self):
         return '{0.name}({0.username})'.format(self)
 
-    def to_json(self):
-        return {
-            'id': self.id,
-            'name': self.name,
-            'username': self.username,
-            'protocol': self.protocol,
-            'priority': self.priority,
-            'auto_push': self.auto_push,
-        }
-
     @property
     def login_mode_display(self):
         return self.get_login_mode_display()
diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py
index b05de01f6..b8295457f 100644
--- a/apps/assets/serializers/admin_user.py
+++ b/apps/assets/serializers/admin_user.py
@@ -21,7 +21,7 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
         model = AdminUser
         fields = [
             'id', 'name', 'username', 'password', 'private_key', 'public_key',
-            'comment', 'connectivity_amount', 'assets_amount',
+            'comment', 'assets_amount',
             'date_created', 'date_updated', 'created_by',
         ]
 
@@ -33,7 +33,6 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
             'date_updated': {'read_only': True},
             'created_by': {'read_only': True},
             'assets_amount': {'label': _('Asset')},
-            'connectivity_amount': {'label': _('Connectivity')},
         }
 
 
diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py
index b33f22116..ea4aa7355 100644
--- a/apps/assets/serializers/node.py
+++ b/apps/assets/serializers/node.py
@@ -17,9 +17,8 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
 
     class Meta:
         model = Node
-        fields = [
-            'id', 'key', 'value', 'assets_amount', 'org_id',
-        ]
+        only_fields = ['id', 'key', 'value', 'org_id']
+        fields = only_fields + ['assets_amount']
         read_only_fields = [
             'key', 'assets_amount', 'org_id',
         ]
diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py
index d853984e2..70855c9f7 100644
--- a/apps/assets/serializers/system_user.py
+++ b/apps/assets/serializers/system_user.py
@@ -21,14 +21,13 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
             'id', 'name', 'username', 'password', 'public_key', 'private_key',
             'login_mode', 'login_mode_display', 'priority', 'protocol',
             'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'nodes',
-            'assets_amount', 'connectivity_amount', 'auto_generate_key'
+            'assets_amount', 'auto_generate_key'
         ]
         extra_kwargs = {
             'password': {"write_only": True},
             'public_key': {"write_only": True},
             'private_key': {"write_only": True},
             'assets_amount': {'label': _('Asset')},
-            'connectivity_amount': {'label': _('Connectivity')},
             'login_mode_display': {'label': _('Login mode display')},
             'created_by': {'read_only': True},
         }
diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py
index 59d01c98a..b2316d7d0 100644
--- a/apps/assets/signals_handler.py
+++ b/apps/assets/signals_handler.py
@@ -78,6 +78,7 @@ def on_system_user_assets_change(sender, instance=None, **kwargs):
 @receiver(m2m_changed, sender=Asset.nodes.through)
 def on_asset_node_changed(sender, instance=None, **kwargs):
     logger.debug("Asset nodes change signal received")
+    Asset.expire_all_nodes_keys_cache()
     if isinstance(instance, Asset):
         if kwargs['action'] == 'pre_remove':
             nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html
index 93aed7d89..61737184c 100644
--- a/apps/assets/templates/assets/_node_tree.html
+++ b/apps/assets/templates/assets/_node_tree.html
@@ -291,42 +291,6 @@ function defaultCallback(action) {
 
 $(document).ready(function () {
 })
-.on('click', '.btn-refresh-hardware', function () {
-    var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
-    var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
-    function success(data) {
-        rMenu.css({"visibility" : "hidden"});
-        var task_id = data.task;
-        var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
-        window.open(url, '', 'width=800,height=600')
-    }
-    APIUpdateAttr({
-        url: the_url,
-        method: "GET",
-        success: success,
-        flash_message: false
-    });
-
-})
-.on('click', '.btn-test-connective', function () {
-    var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
-    if (!current_node_id) {
-        return null;
-    }
-    var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
-    function success(data) {
-        rMenu.css({"visibility" : "hidden"});
-        var task_id = data.task;
-        var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
-        window.open(url, '', 'width=800,height=600')
-    }
-    APIUpdateAttr({
-        url: the_url,
-        method: "GET",
-        success: success,
-        flash_message: false
-    });
-})
 .on('click', '.btn-show-current-asset', function(){
     hideRMenu();
     $(this).css('display', 'none');
@@ -341,17 +305,5 @@ $(document).ready(function () {
     setCookie('show_current_asset', '');
     location.reload();
 })
-.on('click', '.btn-test-connective', function () {
-    hideRMenu();
 
-})
-.on('click', '#menu_refresh_assets_amount', function () {
-    hideRMenu();
-    var url = "{% url 'api-assets:refresh-assets-amount' %}";
-    APIUpdateAttr({
-        'url': url,
-        'method': 'GET'
-    });
-    window.location.reload();
-})
 </script>
\ No newline at end of file
diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html
index 0a17eccaa..61389ad08 100644
--- a/apps/assets/templates/assets/admin_user_list.html
+++ b/apps/assets/templates/assets/admin_user_list.html
@@ -2,8 +2,6 @@
 {% load i18n static %}
 {% block help_message %}
     <div class="alert alert-info help-message">
-{#    管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
-{#    Windows或其它硬件可以随意设置一个#}
     {% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
     {% trans 'Jumpserver users of the system using the user to `push system user`,  `get assets hardware information`, etc. '%}
     {% trans 'You can set any one for Windows or other hardware.' %}
@@ -47,9 +45,9 @@
         <th class="text-center">{% trans 'Name' %}</th>
         <th class="text-center">{% trans 'Username' %}</th>
         <th class="text-center">{% trans 'Asset' %}</th>
-        <th class="text-center">{% trans 'Reachable' %}</th>
-        <th class="text-center">{% trans 'Unreachable' %}</th>
-        <th class="text-center">{% trans 'Ratio' %}</th>
+{#        <th class="text-center">{% trans 'Reachable' %}</th>#}
+{#        <th class="text-center">{% trans 'Unreachable' %}</th>#}
+{#        <th class="text-center">{% trans 'Ratio' %}</th>#}
         <th class="text-center">{% trans 'Comment' %}</th>
         <th class="text-center">{% trans 'Action' %}</th>
     </tr>
@@ -73,44 +71,44 @@ function initTable() {
                 var detail_btn = '<a href="{% url "assets:admin-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
                 return detail_btn.replace('{{ DEFAULT_PK }}', rowData.id);
             }},
-            {targets: 4, createdCell: function (td, cellData) {
-                var innerHtml = "";
-                var data = cellData.reachable;
-                if (data !== 0) {
-                    innerHtml = "<span class='text-navy'>" + data + "</span>";
-                } else {
-                    innerHtml = "<span>" + data + "</span>";
-                }
-                $(td).html(innerHtml)
-            }},
-            {targets: 5, createdCell: function (td, cellData) {
-                var data = cellData.unreachable;
-                var innerHtml = "";
-                if (data !== 0) {
-                    innerHtml = "<span class='text-danger'>" + data + "</span>";
-                } else {
-                    innerHtml = "<span>" + data + "</span>";
-                }
-                $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + data + '">' + innerHtml + '</span>');
-            }},
-            {targets: 6, createdCell: function (td, cellData, rowData) {
-                var val = 0;
-                var innerHtml = "";
-                var total = rowData.assets_amount;
-                var reachable = cellData.reachable;
-                if (total !== 0) {
-                    val = reachable/total * 100;
-                }
-
-                if (val === 100) {
-                    innerHtml = "<span class='text-navy'>" + val + "% </span>";
-                } else {
-                    var num = new Number(val);
-                    innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
-                }
-                $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
-            }},
-            {targets: 8, createdCell: function (td, cellData, rowData) {
+            {#{targets: 4, createdCell: function (td, cellData) {#}
+            {#    var innerHtml = "";#}
+            {#    var data = cellData.reachable;#}
+            {#    if (data !== 0) {#}
+            {#        innerHtml = "<span class='text-navy'>" + data + "</span>";#}
+            {#    } else {#}
+            {#        innerHtml = "<span>" + data + "</span>";#}
+            {#    }#}
+            {#    $(td).html(innerHtml)#}
+            {#}},#}
+            {#{targets: 5, createdCell: function (td, cellData) {#}
+            {#    var data = cellData.unreachable;#}
+            {#    var innerHtml = "";#}
+            {#    if (data !== 0) {#}
+            {#        innerHtml = "<span class='text-danger'>" + data + "</span>";#}
+            {#    } else {#}
+            {#        innerHtml = "<span>" + data + "</span>";#}
+            {#    }#}
+            {#    $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + data + '">' + innerHtml + '</span>');#}
+            {#}},#}
+            {#{targets: 6, createdCell: function (td, cellData, rowData) {#}
+            {#    var val = 0;#}
+            {#    var innerHtml = "";#}
+            {#    var total = rowData.assets_amount;#}
+            {#    var reachable = cellData.reachable;#}
+            {#    if (total !== 0) {#}
+            {#        val = reachable/total * 100;#}
+            {#    }#}
+            {##}
+            {#    if (val === 100) {#}
+            {#        innerHtml = "<span class='text-navy'>" + val + "% </span>";#}
+            {#    } else {#}
+            {#        var num = new Number(val);#}
+            {#        innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";#}
+            {#    }#}
+            {#    $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');#}
+            {#}},#}
+            {targets: 5, createdCell: function (td, cellData, rowData) {
                 var update_btn = '<a href="{% url "assets:admin-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
                 var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
                 $(td).html(update_btn + del_btn)
@@ -118,7 +116,7 @@ function initTable() {
         ajax_url: '{% url "api-assets:admin-user-list" %}',
         columns: [
             {data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" },
-            {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},
+            {#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#}
             {data: "comment"}, {data: "id"}
         ]
     };
diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html
index 3b9df3373..8f889feb7 100644
--- a/apps/assets/templates/assets/asset_list.html
+++ b/apps/assets/templates/assets/asset_list.html
@@ -167,7 +167,7 @@ function initTable() {
             }},
             {targets: 5, createdCell: function (td, cellData, rowData) {
                 var update_btn = '<a href="{% url "assets:asset-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
-                var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_asset_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
+                var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
                 $(td).html(update_btn + del_btn)
             }}
         ],
@@ -325,8 +325,7 @@ $(document).ready(function(){
     }
     window.open(url, '_self');
 })
-
-.on('click', '.btn_asset_delete', function () {
+.on('click', '.btn-asset-delete', function () {
     var $this = $(this);
     var $data_table = $("#asset_list_table").DataTable();
     var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
@@ -513,6 +512,40 @@ $(document).ready(function(){
     update_node_action = "add"
 }).on('click', '#menu_asset_move', function () {
     update_node_action = "move"
+}).on('click', '.btn-test-connective', function () {
+    var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
+    if (!current_node_id) {
+        return null;
+    }
+    var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
+    function success(data) {
+        rMenu.css({"visibility" : "hidden"});
+        var task_id = data.task;
+        var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
+        window.open(url, '', 'width=800,height=600')
+    }
+    APIUpdateAttr({
+        url: the_url,
+        method: "GET",
+        success: success,
+        flash_message: false
+    });
+}).on('click', '.btn-refresh-hardware', function () {
+    var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
+    var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
+    function success(data) {
+        rMenu.css({"visibility" : "hidden"});
+        var task_id = data.task;
+        var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
+        window.open(url, '', 'width=800,height=600')
+    }
+    APIUpdateAttr({
+        url: the_url,
+        method: "GET",
+        success: success,
+        flash_message: false
+    });
+
 })
 
 </script>
diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html
index 680bfa421..6c5ff3339 100644
--- a/apps/assets/templates/assets/system_user_list.html
+++ b/apps/assets/templates/assets/system_user_list.html
@@ -53,9 +53,9 @@
         <th class="text-center">{% trans 'Protocol' %}</th>
         <th class="text-center">{% trans 'Login mode' %}</th>
         <th class="text-center">{% trans 'Asset' %}</th>
-        <th class="text-center">{% trans 'Reachable' %}</th>
-        <th class="text-center">{% trans 'Unreachable' %}</th>
-        <th class="text-center">{% trans 'Ratio' %}</th>
+{#        <th class="text-center">{% trans 'Reachable' %}</th>#}
+{#        <th class="text-center">{% trans 'Unreachable' %}</th>#}
+{#        <th class="text-center">{% trans 'Ratio' %}</th>#}
         <th class="text-center">{% trans 'Comment' %}</th>
         <th class="text-center">{% trans 'Action' %}</th>
     </tr>
@@ -78,44 +78,44 @@ function initTable() {
                 var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
                 $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
             }},
-            {targets: 6, createdCell: function (td, cellData) {
-                var innerHtml = "";
-                var data = cellData.reachable;
-                if (data !== 0) {
-                    innerHtml = "<span class='text-navy'>" + data + "</span>";
-                } else {
-                    innerHtml = "<span>" + data + "</span>";
-                }
-                $(td).html(innerHtml)
-            }},
-            {targets: 7, createdCell: function (td, cellData) {
-                var data = cellData.unreachable;
-                var innerHtml = "";
-                if (data !== 0) {
-                    innerHtml = "<span class='text-danger'>" + data + "</span>";
-                } else {
-                    innerHtml = "<span>" + data + "</span>";
-                }
-                $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + data + '">' + innerHtml + '</span>');
-            }},
-            {targets: 8, createdCell: function (td, cellData, rowData) {
-                var val = 0;
-                var innerHtml = "";
-                var total = rowData.assets_amount;
-                var reachable = cellData.reachable;
-                if (total && total !== 0) {
-                    val = reachable/total * 100;
-                }
-
-                if (val === 100) {
-                    innerHtml = "<span class='text-navy'>" + val + "% </span>";
-                } else {
-                    var num = new Number(val);
-                    innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
-                }
-                $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
-            }},
-            {targets: 10, createdCell: function (td, cellData, rowData) {
+            {#{targets: 6, createdCell: function (td, cellData) {#}
+            {#    var innerHtml = "";#}
+            {#    var data = cellData.reachable;#}
+            {#    if (data !== 0) {#}
+            {#        innerHtml = "<span class='text-navy'>" + data + "</span>";#}
+            {#    } else {#}
+            {#        innerHtml = "<span>" + data + "</span>";#}
+            {#    }#}
+            {#    $(td).html(innerHtml)#}
+            {#}},#}
+            {#{targets: 7, createdCell: function (td, cellData) {#}
+            {#    var data = cellData.unreachable;#}
+            {#    var innerHtml = "";#}
+            {#    if (data !== 0) {#}
+            {#        innerHtml = "<span class='text-danger'>" + data + "</span>";#}
+            {#    } else {#}
+            {#        innerHtml = "<span>" + data + "</span>";#}
+            {#    }#}
+            {#    $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + data + '">' + innerHtml + '</span>');#}
+            {#}},#}
+            {#{targets: 8, createdCell: function (td, cellData, rowData) {#}
+            {#    var val = 0;#}
+            {#    var innerHtml = "";#}
+            {#    var total = rowData.assets_amount;#}
+            {#    var reachable = cellData.reachable;#}
+            {#    if (total && total !== 0) {#}
+            {#        val = reachable/total * 100;#}
+            {#    }#}
+            {##}
+            {#    if (val === 100) {#}
+            {#        innerHtml = "<span class='text-navy'>" + val + "% </span>";#}
+            {#    } else {#}
+            {#        var num = new Number(val);#}
+            {#        innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";#}
+            {#    }#}
+            {#    $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');#}
+            {#}},#}
+            {targets: 7, createdCell: function (td, cellData, rowData) {
                 var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
                 var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
                 $(td).html(update_btn + del_btn)
@@ -124,7 +124,7 @@ function initTable() {
         ajax_url: '{% url "api-assets:system-user-list" %}',
         columns: [
             {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" },
-            {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "comment" }, {data: "id" }
+            {data: "comment" }, {data: "id" }
         ],
         op_html: $('#actions').html()
     };
diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html
index 23d0b34ed..f441baa39 100644
--- a/apps/assets/templates/assets/user_asset_list.html
+++ b/apps/assets/templates/assets/user_asset_list.html
@@ -43,7 +43,6 @@
                            <th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
                            <th class="text-center">{% trans 'Hostname' %}</th>
                            <th class="text-center">{% trans 'IP' %}</th>
-                           <th class="text-center">{% trans 'Active' %}</th>
                            <th class="text-center">{% trans 'System users' %}</th>
                            <th class="text-center">{% trans 'Action' %}</th>
                        </tr>
@@ -62,7 +61,7 @@
 
 {% block custom_foot_js %}
 <script>
-var treeUrl = "{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0&cache_policy=1";
+var treeUrl = "{% url 'api-perms:my-nodes-as-tree' %}?&cache_policy=1";
 var zTree, asset_table, show=0;
 var inited = false;
 var url;
@@ -83,20 +82,13 @@ function initTable() {
                 $(td).html(detail_btn.replace("rowData_id", rowData.id));
                 }},
             {targets: 3, createdCell: function (td, cellData) {
-                if (!cellData) {
-                    $(td).html('<i class="fa fa-times text-danger"></i>')
-                } else {
-                    $(td).html('<i class="fa fa-check text-navy"></i>')
-                }
-            }},
-            {targets: 4, createdCell: function (td, cellData) {
                 var users = [];
                 $.each(cellData, function (id, data) {
                     users.push(data.name);
                 });
                 $(td).html(users.join(', '))
             }},
-            {targets: 5, createdCell: function (td, cellData) {
+            {targets: 4, createdCell: function (td, cellData) {
                 var conn_btn = '<a href="{% url "luna-view" %}?login_to=' +  cellData +'" class="btn btn-xs btn-primary">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
                 $(td).html(conn_btn)
             }}
@@ -104,7 +96,6 @@ function initTable() {
         ajax_url: url,
         columns: [
             {data: "id"}, {data: "hostname" }, {data: "ip" },
-            {data: "is_active", orderable: false },
             {data: "system_users_granted", orderable: false},
             {data: "id", orderable: false}
         ]
diff --git a/apps/assets/utils.py b/apps/assets/utils.py
index b64fea35e..a0de3b481 100644
--- a/apps/assets/utils.py
+++ b/apps/assets/utils.py
@@ -104,7 +104,7 @@ class NodeUtil:
             _node._assets_amount = len(_node._assets)
             delattr(_node, '_assets')
         self.stack.top._children.append(_node)
-        self.stack.top._all_children.extend([_node] + _node._children)
+        self.stack.top._all_children.extend([_node] + _node._all_children)
 
     def init(self):
         all_nodes = self.get_all_nodes()
@@ -145,29 +145,69 @@ class NodeUtil:
     def nodes(self):
         return list(self._nodes.values())
 
+    def get_family_by_key(self, key):
+        tree_nodes = set()
+        node = self.get_node_by_key(key)
+        if not node:
+            return []
+        tree_nodes.update(node._parents)
+        tree_nodes.add(node)
+        tree_nodes.update(node._all_children)
+        return list(tree_nodes)
+
     # 使用给定节点生成一颗树
     # 找到他们的祖先节点
     # 可选找到他们的子孙节点
-    def get_family(self, nodes, with_children=False):
-        tree_nodes = set()
-        for n in nodes:
-            node = self.get_node_by_key(n.key)
-            if not node:
-                continue
-            tree_nodes.update(node._parents)
-            tree_nodes.add(node)
-            if with_children:
-                tree_nodes.update(node._children)
-        return list(tree_nodes)
+    def get_family(self, node):
+        return self.get_family_by_key(node.key)
 
-    def get_nodes_parents(self, nodes, with_self=True):
+    def get_family_keys_by_key(self, key):
+        nodes = self.get_family_by_key(key)
+        return [n.key for n in nodes]
+
+    def get_some_nodes_family_by_keys(self, keys):
+        family = set()
+        for key in keys:
+            family.update(self.get_family_by_key(key))
+        return family
+
+    def get_some_nodes_family_keys_by_keys(self, keys):
+        family = self.get_some_nodes_family_by_keys(keys)
+        return [n.key for n in family]
+
+    def get_nodes_parents_by_key(self, key, with_self=True):
         parents = set()
-        for n in nodes:
-            node = self.get_node_by_key(n.key)
-            parents.update(set(node._parents))
-            if with_self:
-                parents.add(node)
-        return parents
+        node = self.get_node_by_key(key)
+        if not node:
+            return []
+        parents.update(set(node._parents))
+        if with_self:
+            parents.add(node)
+        return list(parents)
+
+    def get_node_parents(self, node, with_self=True):
+        return self.get_nodes_parents_by_key(node.key, with_self=with_self)
+
+    def get_nodes_parents_keys_by_key(self, key, with_self=True):
+        nodes = self.get_nodes_parents_by_key(key, with_self=with_self)
+        return [n.key for n in nodes]
+
+    def get_all_children_by_key(self, key, with_self=True):
+        children = set()
+        node = self.get_node_by_key(key)
+        if not node:
+            return []
+        children.update(set(node._all_children))
+        if with_self:
+            children.add(node)
+        return list(children)
+
+    def get_children(self, node, with_self=True):
+        return self.get_all_children_by_key(node.key, with_self=with_self)
+
+    def get_children_keys_by_key(self, key, with_self=True):
+        nodes = self.get_all_children_by_key(key, with_self=with_self)
+        return [n.key for n in nodes]
 
 
 def test_node_tree():
diff --git a/apps/common/api.py b/apps/common/api.py
index bf41312d7..a3b1938be 100644
--- a/apps/common/api.py
+++ b/apps/common/api.py
@@ -4,11 +4,13 @@ import os
 import uuid
 
 from django.core.cache import cache
+from django.views.decorators.csrf import csrf_exempt
 
 from rest_framework.views import APIView
 from rest_framework.response import Response
 from rest_framework import generics, serializers
 
+from .http import HttpResponseTemporaryRedirect
 from .const import KEY_CACHE_RESOURCES_ID
 
 __all__ = [
@@ -86,3 +88,11 @@ class ResourcesIDCacheApi(APIView):
             cache_key = KEY_CACHE_RESOURCES_ID.format(spm)
             cache.set(cache_key, resources_id, 300)
         return Response({'spm': spm})
+
+
+@csrf_exempt
+def redirect_plural_name_api(request, *args, **kwargs):
+    resource = kwargs.get("resource", "")
+    full_path = request.get_full_path()
+    full_path = full_path.replace(resource, resource+"s", 1)
+    return HttpResponseTemporaryRedirect(full_path)
diff --git a/apps/common/http.py b/apps/common/http.py
new file mode 100644
index 000000000..df6b9a78f
--- /dev/null
+++ b/apps/common/http.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+#
+from django.http import HttpResponse
+from django.utils.encoding import iri_to_uri
+
+
+class HttpResponseTemporaryRedirect(HttpResponse):
+    status_code = 307
+
+    def __init__(self, redirect_to):
+        HttpResponse.__init__(self)
+        self['Location'] = iri_to_uri(redirect_to)
diff --git a/apps/common/tree.py b/apps/common/tree.py
index 13756a35d..e73b43aa6 100644
--- a/apps/common/tree.py
+++ b/apps/common/tree.py
@@ -46,12 +46,17 @@ class TreeNode:
 
     def __gt__(self, other):
         if self.isParent and not other.isParent:
-            return False
+            result = False
         elif not self.isParent and other.isParent:
-            return True
-        if self.pId != other.pId:
-            return self.pId > other.pId
-        return self.name > other.name
+            result = True
+        elif self.pId != other.pId:
+            result = self.pId > other.pId
+        else:
+            result = self.name > other.name
+        return result
+
+    def __le__(self, other):
+        return not self.__gt__(other)
 
     def __eq__(self, other):
         return self.id == other.id
@@ -74,7 +79,7 @@ class Tree:
             raise ValueError("Parent must not be node parent")
         node.pId = parent.id
         parent.isParent = True
-        self.nodes[node.id] = node
+        self.nodes[node.key] = node
 
     def get_nodes(self):
         return sorted(self.nodes.values())
diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py
index fdd4827d3..c0bd771e4 100644
--- a/apps/common/utils/common.py
+++ b/apps/common/utils/common.py
@@ -7,6 +7,7 @@ import logging
 import datetime
 import uuid
 from functools import wraps
+import time
 import copy
 import ipaddress
 
@@ -179,3 +180,18 @@ def random_string(length):
     charset = string.ascii_letters + string.digits
     s = [random.choice(charset) for i in range(length)]
     return ''.join(s)
+
+
+logger = get_logger(__name__)
+
+
+def timeit(func):
+    def wrapper(*args, **kwargs):
+        logger.debug("Start call: {}".format(func.__name__))
+        now = time.time()
+        result = func(*args, **kwargs)
+        using = (time.time() - now) * 1000
+        msg = "Call {} end, using: {:.1f}ms".format(func.__name__, using)
+        logger.debug(msg)
+        return result
+    return wrapper
diff --git a/apps/jumpserver/views.py b/apps/jumpserver/views.py
index 7f7662add..9ad8f7b39 100644
--- a/apps/jumpserver/views.py
+++ b/apps/jumpserver/views.py
@@ -2,7 +2,7 @@ import datetime
 import re
 import time
 
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponseRedirect
 from django.conf import settings
 from django.views.generic import TemplateView, View
 from django.utils import timezone
@@ -13,13 +13,14 @@ from rest_framework.response import Response
 from rest_framework.views import APIView
 from django.views.decorators.csrf import csrf_exempt
 from django.http import HttpResponse
-from django.utils.encoding import iri_to_uri
+
 
 from users.models import User
 from assets.models import Asset
 from terminal.models import Session
 from orgs.utils import current_org
 from common.permissions import PermissionsMixin, IsValidUser
+from common.http import HttpResponseTemporaryRedirect
 
 
 class IndexView(PermissionsMixin, TemplateView):
@@ -203,14 +204,6 @@ class I18NView(View):
 api_url_pattern = re.compile(r'^/api/(?P<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
 
 
-class HttpResponseTemporaryRedirect(HttpResponse):
-    status_code = 307
-
-    def __init__(self, redirect_to):
-        HttpResponse.__init__(self)
-        self['Location'] = iri_to_uri(redirect_to)
-
-
 @csrf_exempt
 def redirect_format_api(request, *args, **kwargs):
     _path, query = request.path, request.GET.urlencode()
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 3ca6363f6..c702b74b1 100644
Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 8406a09cd..e7d79281b 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/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: 2019-07-08 15:32+0800\n"
+"POT-Creation-Date: 2019-07-11 11:23+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"
@@ -76,10 +76,10 @@ msgstr "运行参数"
 #: applications/templates/applications/remote_app_list.html:22
 #: applications/templates/applications/user_remote_app_list.html:18
 #: assets/forms/domain.py:15 assets/forms/label.py:13
-#: assets/models/asset.py:286 assets/models/authbook.py:24
+#: assets/models/asset.py:342 assets/models/authbook.py:24
 #: assets/serializers/admin_user.py:35 assets/serializers/asset_user.py:81
 #: assets/serializers/system_user.py:30
-#: assets/templates/assets/admin_user_list.html:49
+#: assets/templates/assets/admin_user_list.html:47
 #: assets/templates/assets/domain_detail.html:60
 #: assets/templates/assets/domain_list.html:26
 #: assets/templates/assets/label_list.html:16
@@ -95,7 +95,7 @@ msgstr "运行参数"
 #: terminal/templates/terminal/command_list.html:66
 #: terminal/templates/terminal/session_list.html:28
 #: terminal/templates/terminal/session_list.html:72
-#: xpack/plugins/change_auth_plan/forms.py:114
+#: xpack/plugins/change_auth_plan/forms.py:115
 #: xpack/plugins/change_auth_plan/models.py:413
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54
@@ -139,7 +139,7 @@ msgstr "系统用户"
 #: assets/models/cmd_filter.py:20 assets/models/domain.py:20
 #: assets/models/group.py:20 assets/models/label.py:18
 #: assets/templates/assets/admin_user_detail.html:56
-#: assets/templates/assets/admin_user_list.html:47
+#: assets/templates/assets/admin_user_list.html:45
 #: assets/templates/assets/cmd_filter_detail.html:61
 #: assets/templates/assets/cmd_filter_list.html:24
 #: assets/templates/assets/domain_detail.html:56
@@ -173,7 +173,7 @@ msgstr "系统用户"
 #: users/templates/users/user_list.html:35
 #: users/templates/users/user_profile.html:51
 #: users/templates/users/user_pubkey_update.html:53
-#: xpack/plugins/change_auth_plan/forms.py:97
+#: xpack/plugins/change_auth_plan/forms.py:98
 #: xpack/plugins/change_auth_plan/models.py:61
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12
@@ -205,7 +205,7 @@ msgstr "参数"
 
 #: applications/models/remote_app.py:43
 #: applications/templates/applications/remote_app_detail.html:77
-#: assets/models/asset.py:151 assets/models/base.py:36
+#: assets/models/asset.py:221 assets/models/base.py:36
 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:25
 #: assets/models/cmd_filter.py:58 assets/models/group.py:21
 #: assets/templates/assets/admin_user_detail.html:68
@@ -229,7 +229,7 @@ msgstr "创建者"
 # msgstr "创建者"
 #: applications/models/remote_app.py:46
 #: applications/templates/applications/remote_app_detail.html:73
-#: assets/models/asset.py:152 assets/models/base.py:34
+#: assets/models/asset.py:222 assets/models/base.py:34
 #: assets/models/cluster.py:26 assets/models/domain.py:23
 #: assets/models/group.py:22 assets/models/label.py:25
 #: assets/templates/assets/admin_user_detail.html:64
@@ -257,12 +257,12 @@ msgstr "创建日期"
 #: applications/templates/applications/remote_app_detail.html:81
 #: applications/templates/applications/remote_app_list.html:24
 #: applications/templates/applications/user_remote_app_list.html:20
-#: assets/models/asset.py:153 assets/models/base.py:33
+#: assets/models/asset.py:223 assets/models/base.py:33
 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:22
 #: assets/models/cmd_filter.py:55 assets/models/domain.py:21
 #: assets/models/domain.py:53 assets/models/group.py:23
 #: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72
-#: assets/templates/assets/admin_user_list.html:53
+#: assets/templates/assets/admin_user_list.html:51
 #: assets/templates/assets/asset_detail.html:132
 #: assets/templates/assets/cmd_filter_detail.html:65
 #: assets/templates/assets/cmd_filter_list.html:27
@@ -412,8 +412,8 @@ msgstr "详情"
 #: applications/templates/applications/remote_app_list.html:56
 #: assets/templates/assets/_asset_user_list.html:70
 #: assets/templates/assets/admin_user_detail.html:24
-#: assets/templates/assets/admin_user_list.html:29
-#: assets/templates/assets/admin_user_list.html:114
+#: assets/templates/assets/admin_user_list.html:27
+#: assets/templates/assets/admin_user_list.html:112
 #: assets/templates/assets/asset_detail.html:27
 #: assets/templates/assets/asset_list.html:78
 #: assets/templates/assets/asset_list.html:169
@@ -456,7 +456,7 @@ msgstr "更新"
 #: applications/templates/applications/remote_app_detail.html:25
 #: applications/templates/applications/remote_app_list.html:57
 #: assets/templates/assets/admin_user_detail.html:28
-#: assets/templates/assets/admin_user_list.html:115
+#: assets/templates/assets/admin_user_list.html:113
 #: assets/templates/assets/asset_detail.html:31
 #: assets/templates/assets/asset_list.html:170
 #: assets/templates/assets/cmd_filter_detail.html:33
@@ -515,7 +515,7 @@ msgstr "创建远程应用"
 #: applications/templates/applications/user_remote_app_list.html:21
 #: assets/models/cmd_filter.py:54
 #: assets/templates/assets/_asset_user_list.html:20
-#: assets/templates/assets/admin_user_list.html:54
+#: assets/templates/assets/admin_user_list.html:52
 #: assets/templates/assets/asset_list.html:100
 #: assets/templates/assets/cmd_filter_list.html:28
 #: assets/templates/assets/cmd_filter_rule_list.html:63
@@ -598,25 +598,21 @@ msgid "Test if the assets under the node are connectable: {}"
 msgstr "测试节点下资产是否可连接: {}"
 
 #: assets/const.py:77 assets/models/utils.py:43
-#: assets/templates/assets/admin_user_list.html:51
-#: assets/templates/assets/system_user_list.html:57
 msgid "Unreachable"
 msgstr "不可达"
 
 #: assets/const.py:78 assets/models/utils.py:44
-#: assets/templates/assets/admin_user_list.html:50
 #: assets/templates/assets/asset_list.html:99
-#: assets/templates/assets/system_user_list.html:56
 #: users/templates/users/user_group_granted_asset.html:47
 msgid "Reachable"
 msgstr "可连接"
 
 #: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:9
-#: xpack/plugins/license/models.py:79
+#: xpack/plugins/license/models.py:78
 msgid "Unknown"
 msgstr "未知"
 
-#: assets/forms/asset.py:24 assets/models/asset.py:117
+#: assets/forms/asset.py:24 assets/models/asset.py:187
 #: assets/models/domain.py:50
 #: assets/templates/assets/domain_gateway_list.html:69
 #: assets/templates/assets/user_asset_list.html:168
@@ -624,7 +620,7 @@ msgstr "未知"
 msgid "Port"
 msgstr "端口"
 
-#: assets/forms/asset.py:45 assets/models/asset.py:122
+#: assets/forms/asset.py:45 assets/models/asset.py:192
 #: assets/models/user.py:107 assets/templates/assets/asset_detail.html:190
 #: assets/templates/assets/asset_detail.html:198
 #: assets/templates/assets/system_user_assets.html:83
@@ -633,7 +629,7 @@ msgstr "端口"
 msgid "Nodes"
 msgstr "节点"
 
-#: assets/forms/asset.py:48 assets/forms/asset.py:83 assets/models/asset.py:126
+#: assets/forms/asset.py:48 assets/forms/asset.py:83 assets/models/asset.py:196
 #: assets/models/cluster.py:19 assets/models/user.py:65
 #: assets/templates/assets/asset_detail.html:76 templates/_nav.html:24
 #: xpack/plugins/cloud/models.py:124
@@ -651,7 +647,7 @@ msgstr "管理用户"
 msgid "Label"
 msgstr "标签"
 
-#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:121
+#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:191
 #: assets/models/domain.py:26 assets/models/domain.py:52
 #: assets/templates/assets/asset_detail.html:80
 #: assets/templates/assets/user_asset_list.html:173
@@ -660,14 +656,14 @@ msgid "Domain"
 msgstr "网域"
 
 #: assets/forms/asset.py:58 assets/forms/asset.py:80 assets/forms/asset.py:93
-#: assets/forms/asset.py:128 assets/models/node.py:255
+#: assets/forms/asset.py:128 assets/models/node.py:254
 #: assets/templates/assets/asset_create.html:42
 #: perms/forms/asset_permission.py:71 perms/forms/asset_permission.py:78
 #: perms/models/asset_permission.py:101
 #: perms/templates/perms/asset_permission_list.html:49
 #: perms/templates/perms/asset_permission_list.html:70
 #: perms/templates/perms/asset_permission_list.html:120
-#: xpack/plugins/change_auth_plan/forms.py:115
+#: xpack/plugins/change_auth_plan/forms.py:116
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:55
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:15
 #: xpack/plugins/cloud/models.py:123
@@ -696,7 +692,7 @@ msgstr "如果有多个的互相隔离的网络,设置资产属于的网域,
 #: assets/forms/asset.py:108 assets/forms/asset.py:112
 #: assets/forms/domain.py:17 assets/forms/label.py:15
 #: perms/templates/perms/asset_permission_asset.html:88
-#: xpack/plugins/change_auth_plan/forms.py:105
+#: xpack/plugins/change_auth_plan/forms.py:106
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:84
 msgid "Select assets"
 msgstr "选择资产"
@@ -719,7 +715,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC"
 #: assets/templates/assets/_asset_user_auth_view_modal.html:21
 #: assets/templates/assets/_asset_user_list.html:16
 #: assets/templates/assets/admin_user_detail.html:60
-#: assets/templates/assets/admin_user_list.html:48
+#: assets/templates/assets/admin_user_list.html:46
 #: assets/templates/assets/domain_gateway_list.html:71
 #: assets/templates/assets/system_user_detail.html:62
 #: assets/templates/assets/system_user_list.html:52 audits/models.py:94
@@ -734,7 +730,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC"
 #: users/templates/users/user_detail.html:67
 #: users/templates/users/user_list.html:36
 #: users/templates/users/user_profile.html:47
-#: xpack/plugins/change_auth_plan/forms.py:99
+#: xpack/plugins/change_auth_plan/forms.py:100
 #: xpack/plugins/change_auth_plan/models.py:63
 #: xpack/plugins/change_auth_plan/models.py:409
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65
@@ -809,7 +805,7 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写"
 msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig"
 msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig"
 
-#: assets/models/asset.py:112 assets/models/domain.py:49
+#: assets/models/asset.py:182 assets/models/domain.py:49
 #: assets/serializers/asset_user.py:28
 #: assets/templates/assets/_asset_list_modal.html:46
 #: assets/templates/assets/_asset_user_list.html:15
@@ -826,7 +822,7 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig"
 msgid "IP"
 msgstr "IP"
 
-#: assets/models/asset.py:113 assets/serializers/asset_user.py:27
+#: assets/models/asset.py:183 assets/serializers/asset_user.py:27
 #: assets/templates/assets/_asset_list_modal.html:45
 #: assets/templates/assets/_asset_user_auth_update_modal.html:9
 #: assets/templates/assets/_asset_user_auth_view_modal.html:15
@@ -843,101 +839,100 @@ msgstr "IP"
 msgid "Hostname"
 msgstr "主机名"
 
-#: assets/models/asset.py:116 assets/models/domain.py:51
+#: assets/models/asset.py:186 assets/models/domain.py:51
 #: assets/models/user.py:110 assets/templates/assets/asset_detail.html:72
 #: assets/templates/assets/domain_gateway_list.html:70
 #: assets/templates/assets/system_user_detail.html:70
 #: assets/templates/assets/system_user_list.html:53
 #: assets/templates/assets/user_asset_list.html:169
 #: terminal/templates/terminal/session_list.html:31
-#: terminal/templates/terminal/session_list.html:75
 msgid "Protocol"
 msgstr "协议"
 
-#: assets/models/asset.py:119 assets/serializers/asset.py:63
+#: assets/models/asset.py:189 assets/serializers/asset.py:63
 #: assets/templates/assets/asset_create.html:24
 msgid "Protocols"
 msgstr "协议组"
 
-#: assets/models/asset.py:120 assets/templates/assets/asset_detail.html:104
+#: assets/models/asset.py:190 assets/templates/assets/asset_detail.html:104
 #: assets/templates/assets/user_asset_list.html:170
 msgid "Platform"
 msgstr "系统平台"
 
-#: assets/models/asset.py:123 assets/models/cmd_filter.py:21
+#: assets/models/asset.py:193 assets/models/cmd_filter.py:21
 #: assets/models/domain.py:54 assets/models/label.py:22
 #: assets/templates/assets/asset_detail.html:112
 #: assets/templates/assets/user_asset_list.html:174
 msgid "Is active"
 msgstr "激活"
 
-#: assets/models/asset.py:129 assets/templates/assets/asset_detail.html:68
+#: assets/models/asset.py:199 assets/templates/assets/asset_detail.html:68
 msgid "Public IP"
 msgstr "公网IP"
 
-#: assets/models/asset.py:130 assets/templates/assets/asset_detail.html:120
+#: assets/models/asset.py:200 assets/templates/assets/asset_detail.html:120
 msgid "Asset number"
 msgstr "资产编号"
 
-#: assets/models/asset.py:133 assets/templates/assets/asset_detail.html:84
+#: assets/models/asset.py:203 assets/templates/assets/asset_detail.html:84
 msgid "Vendor"
 msgstr "制造商"
 
-#: assets/models/asset.py:134 assets/templates/assets/asset_detail.html:88
+#: assets/models/asset.py:204 assets/templates/assets/asset_detail.html:88
 msgid "Model"
 msgstr "型号"
 
-#: assets/models/asset.py:135 assets/templates/assets/asset_detail.html:116
+#: assets/models/asset.py:205 assets/templates/assets/asset_detail.html:116
 msgid "Serial number"
 msgstr "序列号"
 
-#: assets/models/asset.py:137
+#: assets/models/asset.py:207
 msgid "CPU model"
 msgstr "CPU型号"
 
-#: assets/models/asset.py:138
+#: assets/models/asset.py:208
 #: xpack/plugins/license/templates/license/license_detail.html:80
 msgid "CPU count"
 msgstr "CPU数量"
 
-#: assets/models/asset.py:139
+#: assets/models/asset.py:209
 msgid "CPU cores"
 msgstr "CPU核数"
 
-#: assets/models/asset.py:140
+#: assets/models/asset.py:210
 msgid "CPU vcpus"
 msgstr "CPU总数"
 
-#: assets/models/asset.py:141 assets/templates/assets/asset_detail.html:96
+#: assets/models/asset.py:211 assets/templates/assets/asset_detail.html:96
 msgid "Memory"
 msgstr "内存"
 
-#: assets/models/asset.py:142
+#: assets/models/asset.py:212
 msgid "Disk total"
 msgstr "硬盘大小"
 
-#: assets/models/asset.py:143
+#: assets/models/asset.py:213
 msgid "Disk info"
 msgstr "硬盘信息"
 
-#: assets/models/asset.py:145 assets/templates/assets/asset_detail.html:108
+#: assets/models/asset.py:215 assets/templates/assets/asset_detail.html:108
 #: assets/templates/assets/user_asset_list.html:171
 msgid "OS"
 msgstr "操作系统"
 
-#: assets/models/asset.py:146
+#: assets/models/asset.py:216
 msgid "OS version"
 msgstr "系统版本"
 
-#: assets/models/asset.py:147
+#: assets/models/asset.py:217
 msgid "OS arch"
 msgstr "系统架构"
 
-#: assets/models/asset.py:148
+#: assets/models/asset.py:218
 msgid "Hostname raw"
 msgstr "主机名原始"
 
-#: assets/models/asset.py:150 assets/templates/assets/asset_create.html:46
+#: assets/models/asset.py:220 assets/templates/assets/asset_create.html:46
 #: assets/templates/assets/asset_detail.html:227 templates/_nav.html:26
 msgid "Labels"
 msgstr "标签管理"
@@ -1003,7 +998,6 @@ msgid "Operator"
 msgstr "运营商"
 
 #: assets/models/cluster.py:36 assets/models/group.py:34
-#: perms/utils/asset_permission.py:106
 msgid "Default"
 msgstr "默认"
 
@@ -1136,7 +1130,7 @@ msgstr "默认资产组"
 msgid "User"
 msgstr "用户"
 
-#: assets/models/label.py:19 assets/models/node.py:246
+#: assets/models/label.py:19 assets/models/node.py:245
 #: assets/templates/assets/label_list.html:15 settings/models.py:30
 msgid "Value"
 msgstr "值"
@@ -1145,11 +1139,11 @@ msgstr "值"
 msgid "Category"
 msgstr "分类"
 
-#: assets/models/node.py:245
+#: assets/models/node.py:244
 msgid "Key"
 msgstr "键"
 
-#: assets/models/node.py:303
+#: assets/models/node.py:302
 msgid "New node"
 msgstr "新节点"
 
@@ -1208,12 +1202,6 @@ msgstr "登录模式"
 msgid "%(value)s is not an even number"
 msgstr "%(value)s is not an even number"
 
-#: assets/serializers/admin_user.py:36 assets/serializers/asset.py:64
-#: assets/serializers/asset_user.py:29 assets/serializers/system_user.py:31
-#: assets/templates/assets/_asset_user_list.html:18
-msgid "Connectivity"
-msgstr "连接"
-
 #: assets/serializers/asset.py:21
 msgid "Protocol format should {}/{}"
 msgstr "协议格式 {}/{}"
@@ -1222,6 +1210,11 @@ msgstr "协议格式 {}/{}"
 msgid "Protocol duplicate: {}"
 msgstr "协议重复: {}"
 
+#: assets/serializers/asset.py:64 assets/serializers/asset_user.py:29
+#: assets/templates/assets/_asset_user_list.html:18
+msgid "Connectivity"
+msgstr "连接"
+
 #: assets/serializers/asset.py:90
 msgid "Hardware info"
 msgstr "硬件信息"
@@ -1247,19 +1240,19 @@ msgstr "ssh公钥"
 msgid "private key invalid"
 msgstr "密钥不合法"
 
-#: assets/serializers/node.py:33
+#: assets/serializers/node.py:32
 msgid "The same level node name cannot be the same"
 msgstr "同级别节点名字不能重复"
 
-#: assets/serializers/system_user.py:32
+#: assets/serializers/system_user.py:31
 msgid "Login mode display"
 msgstr "登录模式显示"
 
-#: assets/serializers/system_user.py:67
+#: assets/serializers/system_user.py:66
 msgid "* Automatic login mode must fill in the username."
 msgstr "自动登录模式,必须填写用户名"
 
-#: assets/serializers/system_user.py:76
+#: assets/serializers/system_user.py:75
 msgid "Password or private key required"
 msgstr "密码或密钥密码需要一个"
 
@@ -1401,7 +1394,7 @@ msgid "Update asset user auth"
 msgstr "更新资产用户认证信息"
 
 #: assets/templates/assets/_asset_user_auth_update_modal.html:23
-#: xpack/plugins/change_auth_plan/forms.py:101
+#: xpack/plugins/change_auth_plan/forms.py:102
 msgid "Please input password"
 msgstr "请输入密码"
 
@@ -1490,19 +1483,19 @@ msgstr "重命名节点"
 msgid "Delete node"
 msgstr "删除节点"
 
-#: assets/templates/assets/_node_tree.html:155
+#: assets/templates/assets/_node_tree.html:154
 msgid "Create node failed"
 msgstr "创建节点失败"
 
-#: assets/templates/assets/_node_tree.html:167
+#: assets/templates/assets/_node_tree.html:166
 msgid "Have child node, cancel"
 msgstr "存在子节点,不能删除"
 
-#: assets/templates/assets/_node_tree.html:169
+#: assets/templates/assets/_node_tree.html:168
 msgid "Have assets, cancel"
 msgstr "存在资产,不能删除"
 
-#: assets/templates/assets/_node_tree.html:243
+#: assets/templates/assets/_node_tree.html:242
 msgid "Rename success"
 msgstr "重命名成功"
 
@@ -1582,14 +1575,14 @@ msgstr "替换资产的管理员"
 
 #: assets/templates/assets/admin_user_detail.html:91
 #: perms/templates/perms/asset_permission_asset.html:116
-#: xpack/plugins/change_auth_plan/forms.py:109
+#: xpack/plugins/change_auth_plan/forms.py:110
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:112
 msgid "Select nodes"
 msgstr "选择节点"
 
 #: assets/templates/assets/admin_user_detail.html:100
 #: assets/templates/assets/asset_detail.html:207
-#: assets/templates/assets/asset_list.html:396
+#: assets/templates/assets/asset_list.html:395
 #: assets/templates/assets/cmd_filter_detail.html:106
 #: assets/templates/assets/system_user_assets.html:100
 #: assets/templates/assets/system_user_detail.html:182
@@ -1611,24 +1604,24 @@ msgstr "选择节点"
 msgid "Confirm"
 msgstr "确认"
 
-#: assets/templates/assets/admin_user_list.html:7
+#: assets/templates/assets/admin_user_list.html:5
 msgid ""
 "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL "
 "sudo permissions users, "
 msgstr ""
 "管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,"
 
-#: assets/templates/assets/admin_user_list.html:8
+#: assets/templates/assets/admin_user_list.html:6
 msgid ""
 "Jumpserver users of the system using the user to `push system user`,  `get "
 "assets hardware information`, etc. "
 msgstr "Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。"
 
-#: assets/templates/assets/admin_user_list.html:9
+#: assets/templates/assets/admin_user_list.html:7
 msgid "You can set any one for Windows or other hardware."
 msgstr "Windows或其它硬件可以随意设置一个"
 
-#: assets/templates/assets/admin_user_list.html:19
+#: assets/templates/assets/admin_user_list.html:17
 #: assets/templates/assets/asset_list.html:68
 #: assets/templates/assets/system_user_list.html:23
 #: audits/templates/audits/login_log_list.html:85
@@ -1638,7 +1631,7 @@ msgstr "Windows或其它硬件可以随意设置一个"
 msgid "Export"
 msgstr "导出"
 
-#: assets/templates/assets/admin_user_list.html:24
+#: assets/templates/assets/admin_user_list.html:22
 #: assets/templates/assets/asset_list.html:73
 #: assets/templates/assets/system_user_list.html:28
 #: settings/templates/settings/_ldap_list_users_modal.html:100
@@ -1649,20 +1642,13 @@ msgstr "导出"
 msgid "Import"
 msgstr "导入"
 
-#: assets/templates/assets/admin_user_list.html:39
+#: assets/templates/assets/admin_user_list.html:37
 #: assets/views/admin_user.py:50
 msgid "Create admin user"
 msgstr "创建管理用户"
 
-#: assets/templates/assets/admin_user_list.html:52
-#: assets/templates/assets/system_user_list.html:58
-#: ops/templates/ops/adhoc_history.html:54
-#: ops/templates/ops/task_history.html:60
-msgid "Ratio"
-msgstr "比例"
-
-#: assets/templates/assets/admin_user_list.html:165
-#: assets/templates/assets/admin_user_list.html:196
+#: assets/templates/assets/admin_user_list.html:163
+#: assets/templates/assets/admin_user_list.html:194
 #: assets/templates/assets/asset_list.html:268
 #: assets/templates/assets/asset_list.html:305
 #: assets/templates/assets/system_user_list.html:225
@@ -1807,7 +1793,7 @@ msgstr "仅显示当前节点资产"
 msgid "Displays all child node assets"
 msgstr "显示所有子节点资产"
 
-#: assets/templates/assets/asset_list.html:390
+#: assets/templates/assets/asset_list.html:389
 #: assets/templates/assets/system_user_list.html:166
 #: users/templates/users/user_detail.html:382
 #: users/templates/users/user_detail.html:408
@@ -1818,11 +1804,11 @@ msgstr "显示所有子节点资产"
 msgid "Are you sure?"
 msgstr "你确认吗?"
 
-#: assets/templates/assets/asset_list.html:391
+#: assets/templates/assets/asset_list.html:390
 msgid "This will delete the selected assets !!!"
 msgstr "删除选择资产"
 
-#: assets/templates/assets/asset_list.html:394
+#: assets/templates/assets/asset_list.html:393
 #: assets/templates/assets/system_user_list.html:170
 #: settings/templates/settings/terminal_setting.html:166
 #: users/templates/users/user_detail.html:386
@@ -1836,16 +1822,16 @@ msgstr "删除选择资产"
 msgid "Cancel"
 msgstr "取消"
 
-#: assets/templates/assets/asset_list.html:407
+#: assets/templates/assets/asset_list.html:406
 msgid "Asset Deleted."
 msgstr "已被删除"
 
-#: assets/templates/assets/asset_list.html:408
-#: assets/templates/assets/asset_list.html:412
+#: assets/templates/assets/asset_list.html:407
+#: assets/templates/assets/asset_list.html:411
 msgid "Asset Delete"
 msgstr "删除"
 
-#: assets/templates/assets/asset_list.html:411
+#: assets/templates/assets/asset_list.html:410
 msgid "Asset Deleting failed."
 msgstr "删除失败"
 
@@ -2650,7 +2636,7 @@ msgstr "不能包含特殊字符"
 msgid "This field must be unique."
 msgstr "字段必须唯一"
 
-#: jumpserver/views.py:190
+#: jumpserver/views.py:191
 msgid ""
 "<div>Luna is a separately deployed program, you need to deploy Luna, coco, "
 "configure nginx for url distribution,</div> </div>If you see this page, "
@@ -2853,6 +2839,11 @@ msgstr "执行历史"
 msgid "F/S/T"
 msgstr "失败/成功/总"
 
+#: ops/templates/ops/adhoc_history.html:54
+#: ops/templates/ops/task_history.html:60
+msgid "Ratio"
+msgstr "比例"
+
 #: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:142
 msgid "Run history detail"
 msgstr "执行历史详情"
@@ -3005,6 +2996,14 @@ msgstr "命令执行"
 msgid "Organization"
 msgstr "组织"
 
+#: perms/api/user_permission.py:206
+msgid "ungrouped"
+msgstr "未分组"
+
+#: perms/api/user_permission.py:211
+msgid "empty"
+msgstr "空"
+
 #: perms/forms/asset_permission.py:65 perms/forms/remote_app_permission.py:34
 #: perms/models/asset_permission.py:102 perms/models/base.py:37
 #: perms/templates/perms/asset_permission_list.html:47
@@ -3207,10 +3206,6 @@ msgstr "添加用户"
 msgid "Add user group to this permission"
 msgstr "添加用户组"
 
-#: perms/utils/asset_permission.py:115
-msgid "Empty"
-msgstr "空"
-
 #: perms/views/asset_permission.py:33 perms/views/asset_permission.py:64
 #: perms/views/asset_permission.py:81 perms/views/asset_permission.py:98
 #: perms/views/asset_permission.py:135 perms/views/asset_permission.py:168
@@ -5313,25 +5308,28 @@ msgid "Password length"
 msgstr "密码长度"
 
 #: xpack/plugins/change_auth_plan/forms.py:45
-msgid "* For security, please do not change root user's password"
-msgstr "* 为了安全,请不要更改root用户的密码"
+#: xpack/plugins/change_auth_plan/models.py:213
+#, fuzzy
+#| msgid "For security, do not change {} user's password"
+msgid "* For security, do not change {} user's password"
+msgstr "* 为了安全,禁止更改 {} 用户的密码"
 
-#: xpack/plugins/change_auth_plan/forms.py:54
+#: xpack/plugins/change_auth_plan/forms.py:55
 msgid "* Please enter custom password"
 msgstr "* 请输入自定义密码"
 
-#: xpack/plugins/change_auth_plan/forms.py:63
+#: xpack/plugins/change_auth_plan/forms.py:64
 msgid "* Please enter a valid crontab expression"
 msgstr "* 请输入有效的 crontab 表达式"
 
-#: xpack/plugins/change_auth_plan/forms.py:116
+#: xpack/plugins/change_auth_plan/forms.py:117
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:60
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:81
 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17
 msgid "Periodic perform"
 msgstr "定时执行"
 
-#: xpack/plugins/change_auth_plan/forms.py:120
+#: xpack/plugins/change_auth_plan/forms.py:121
 msgid ""
 "Tips: The username of the user on the asset to be modified. if the user "
 "exists, change the password; If the user does not exist, create the user."
@@ -5339,11 +5337,11 @@ msgstr ""
 "提示:用户名为将要修改的资产上的用户的用户名。如果用户存在,则修改密码;如果"
 "用户不存在,则创建用户。"
 
-#: xpack/plugins/change_auth_plan/forms.py:124
+#: xpack/plugins/change_auth_plan/forms.py:125
 msgid "Tips: (Units: hour)"
 msgstr "提示:(单位: 时)"
 
-#: xpack/plugins/change_auth_plan/forms.py:125
+#: xpack/plugins/change_auth_plan/forms.py:126
 msgid ""
 "eg: Every Sunday 03:05 run <5 3 * * 0> <br> Tips: Using 5 digits linux "
 "crontab expressions <min hour day month week> (<a href='https://tool.lu/"
@@ -5396,10 +5394,6 @@ msgstr "定期执行"
 msgid "Password rules"
 msgstr "密码规则"
 
-#: xpack/plugins/change_auth_plan/models.py:213
-msgid "For security, do not change {} user's password"
-msgstr "* 为了安全,禁止更改 {} 用户的密码"
-
 #: xpack/plugins/change_auth_plan/models.py:217
 msgid "Assets is empty, please add the asset"
 msgstr "资产为空,请添加资产"
@@ -5768,7 +5762,7 @@ msgid "Interface settings"
 msgstr "界面设置"
 
 #: xpack/plugins/interface/templates/interface/interface.html:15
-#: xpack/plugins/interface/views.py:25
+#: xpack/plugins/interface/views.py:24 xpack/plugins/interface/views.py:25
 msgid "Interface setting"
 msgstr "界面设置"
 
@@ -5791,26 +5785,22 @@ msgstr "恢复默认成功!"
 msgid "Restore default failed."
 msgstr "恢复默认失败!"
 
-#: xpack/plugins/interface/views.py:24
-msgid "Interface"
-msgstr "界面"
-
 #: xpack/plugins/interface/views.py:51
 msgid "It is already in the default setting state!"
 msgstr "当前已经是初始化状态!"
 
-#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:98
+#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:94
 #: xpack/plugins/license/templates/license/license_detail.html:50
 #: xpack/plugins/license/templates/license/license_detail.html:55
 #: xpack/plugins/license/views.py:32
 msgid "License"
 msgstr "许可证"
 
-#: xpack/plugins/license/models.py:75
+#: xpack/plugins/license/models.py:74
 msgid "Standard edition"
 msgstr "标准版"
 
-#: xpack/plugins/license/models.py:77
+#: xpack/plugins/license/models.py:76
 msgid "Enterprise edition"
 msgstr "企业版"
 
@@ -5898,7 +5888,9 @@ msgstr "无效的许可证"
 msgid "Admin"
 msgstr "管理员"
 
-#: xpack/plugins/orgs/meta.py:8
+#: xpack/plugins/orgs/meta.py:8 xpack/plugins/orgs/views.py:26
+#: xpack/plugins/orgs/views.py:43 xpack/plugins/orgs/views.py:60
+#: xpack/plugins/orgs/views.py:77
 msgid "Organizations"
 msgstr "组织管理"
 
@@ -5915,11 +5907,6 @@ msgstr "添加管理员"
 msgid "Create organization "
 msgstr "创建组织"
 
-#: xpack/plugins/orgs/views.py:26 xpack/plugins/orgs/views.py:43
-#: xpack/plugins/orgs/views.py:60 xpack/plugins/orgs/views.py:77
-msgid "Orgs"
-msgstr "组织管理"
-
 #: xpack/plugins/orgs/views.py:27
 msgid "Org list"
 msgstr "组织列表"
@@ -5949,6 +5936,15 @@ msgstr "密码匣子"
 msgid "vault create"
 msgstr "创建"
 
+#~ msgid "* For security, please do not change root user's password"
+#~ msgstr "* 为了安全,请不要更改root用户的密码"
+
+#~ msgid "Interface"
+#~ msgstr "界面"
+
+#~ msgid "Orgs"
+#~ msgstr "组织管理"
+
 #~ msgid "Org"
 #~ msgstr "组织"
 
diff --git a/apps/perms/api/remote_app_permission.py b/apps/perms/api/remote_app_permission.py
index d75e80b8f..2e3b851f7 100644
--- a/apps/perms/api/remote_app_permission.py
+++ b/apps/perms/api/remote_app_permission.py
@@ -99,3 +99,4 @@ class RemoteAppPermissionRemoveRemoteAppApi(generics.RetrieveUpdateAPIView):
         else:
             return Response({"error": serializer.errors})
 
+
diff --git a/apps/perms/api/user_group_permission.py b/apps/perms/api/user_group_permission.py
index 4d59c13a7..9e32055d4 100644
--- a/apps/perms/api/user_group_permission.py
+++ b/apps/perms/api/user_group_permission.py
@@ -7,148 +7,57 @@ from rest_framework.generics import (
 )
 
 from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
-from common.tree import TreeNodeSerializer
-from ..utils import (
-    AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node,
-    RemoteAppPermissionUtil,
-)
-from ..hands import (
-    UserGroup,  Node, NodeSerializer, RemoteAppSerializer,
-)
+from ..hands import UserGroup
 from .. import serializers, const
 
+from .user_permission import (
+    UserGrantedAssetsApi, UserGrantedNodesApi, UserGrantedNodesWithAssetsApi,
+    UserGrantedNodesWithAssetsAsTreeApi, UserGrantedNodeAssetsApi,
+)
 
 __all__ = [
     'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi',
     'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi',
     'UserGroupGrantedNodesWithAssetsAsTreeApi',
-    'UserGroupGrantedRemoteAppsApi',
 ]
 
 
-class UserGroupGrantedAssetsApi(ListAPIView):
-    permission_classes = (IsOrgAdmin,)
-    serializer_class = serializers.AssetGrantedSerializer
-
-    def get_queryset(self):
+class UserGroupGrantedAssetsApi(UserGrantedAssetsApi):
+    def get_object(self):
         user_group_id = self.kwargs.get('pk', '')
-        queryset = []
-
-        if not user_group_id:
-            return queryset
-
         user_group = get_object_or_404(UserGroup, id=user_group_id)
-        util = AssetPermissionUtil(user_group)
-        assets = util.get_assets()
-        for k, v in assets.items():
-            k.system_users_granted = v
-            queryset.append(k)
-        return queryset
+        return user_group
 
 
-class UserGroupGrantedNodesApi(ListAPIView):
-    permission_classes = (IsOrgAdmin,)
-    serializer_class = NodeSerializer
-
-    def get_queryset(self):
-        group_id = self.kwargs.get('pk', '')
-        queryset = []
-
-        if group_id:
-            group = get_object_or_404(UserGroup, id=group_id)
-            util = AssetPermissionUtil(group)
-            nodes = util.get_nodes_with_assets()
-            return nodes.keys()
-        return queryset
+class UserGroupGrantedNodesApi(UserGrantedNodesApi):
+    def get_object(self):
+        user_group_id = self.kwargs.get('pk', '')
+        user_group = get_object_or_404(UserGroup, id=user_group_id)
+        return user_group
 
 
-class UserGroupGrantedNodesWithAssetsApi(ListAPIView):
+class UserGroupGrantedNodesWithAssetsApi(UserGrantedNodesWithAssetsApi):
     permission_classes = (IsOrgAdmin,)
     serializer_class = serializers.NodeGrantedSerializer
 
-    def get_queryset(self):
+    def get_object(self):
         user_group_id = self.kwargs.get('pk', '')
-        queryset = []
-
-        if not user_group_id:
-            return queryset
-
         user_group = get_object_or_404(UserGroup, id=user_group_id)
-        util = AssetPermissionUtil(user_group)
-        nodes = util.get_nodes_with_assets()
-        for node, _assets in nodes.items():
-            assets = _assets.keys()
-            for asset, system_users in _assets.items():
-                asset.system_users_granted = system_users
-            node.assets_granted = assets
-            queryset.append(node)
-        return queryset
+        return user_group
 
 
-class UserGroupGrantedNodesWithAssetsAsTreeApi(ListAPIView):
-    serializer_class = TreeNodeSerializer
-    permission_classes = (IsOrgAdminOrAppUser,)
-    show_assets = True
-    system_user_id = None
-
-    def get(self, request, *args, **kwargs):
-        self.show_assets = request.query_params.get('show_assets', '1') == '1'
-        self.system_user_id = request.query_params.get('system_user')
-        return super().get(request, *args, **kwargs)
-
-    def get_queryset(self):
+class UserGroupGrantedNodesWithAssetsAsTreeApi(UserGrantedNodesWithAssetsAsTreeApi):
+    def get_object(self):
         user_group_id = self.kwargs.get('pk', '')
-        queryset = []
-        group = get_object_or_404(UserGroup, id=user_group_id)
-        util = AssetPermissionUtil(group)
-        if self.system_user_id:
-            util.filter_permissions(system_users=self.system_user_id)
-        nodes = util.get_nodes_with_assets()
-        for node, assets in nodes.items():
-            data = parse_node_to_tree_node(node)
-            queryset.append(data)
-            if not self.show_assets:
-                continue
-            for asset, system_users in assets.items():
-                data = parse_asset_to_tree_node(node, asset, system_users)
-                queryset.append(data)
-        queryset = sorted(queryset)
-        return queryset
+        user_group = get_object_or_404(UserGroup, id=user_group_id)
+        return user_group
 
 
-class UserGroupGrantedNodeAssetsApi(ListAPIView):
+class UserGroupGrantedNodeAssetsApi(UserGrantedNodeAssetsApi):
     permission_classes = (IsOrgAdminOrAppUser,)
     serializer_class = serializers.AssetGrantedSerializer
 
-    def get_queryset(self):
+    def get_object(self):
         user_group_id = self.kwargs.get('pk', '')
-        node_id = self.kwargs.get('node_id')
-
         user_group = get_object_or_404(UserGroup, id=user_group_id)
-        util = AssetPermissionUtil(user_group)
-        if str(node_id) == const.UNGROUPED_NODE_ID:
-            node = util.tree.ungrouped_node
-        else:
-            node = get_object_or_404(Node, id=node_id)
-        nodes = util.get_nodes_with_assets()
-        assets = nodes.get(node, [])
-        for asset, system_users in assets.items():
-            asset.system_users_granted = system_users
-        return assets
-
-
-# RemoteApp permission
-
-class UserGroupGrantedRemoteAppsApi(ListAPIView):
-    permission_classes = (IsOrgAdmin, )
-    serializer_class = RemoteAppSerializer
-
-    def get_queryset(self):
-        queryset = []
-        user_group_id = self.kwargs.get('pk')
-        if not user_group_id:
-            return queryset
-        user_group = get_object_or_404(UserGroup, id=user_group_id)
-        util = RemoteAppPermissionUtil(user_group)
-        queryset = util.get_remote_apps()
-        return queryset
+        return user_group
diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py
index 3eafc28e0..8d55ee49c 100644
--- a/apps/perms/api/user_permission.py
+++ b/apps/perms/api/user_permission.py
@@ -2,40 +2,44 @@
 #
 import time
 import traceback
+import uuid
 from hashlib import md5
 from django.core.cache import cache
 from django.conf import settings
+from django.db.models import Q
 from django.shortcuts import get_object_or_404
 from rest_framework.views import APIView, Response
 from rest_framework.generics import (
     ListAPIView, get_object_or_404, RetrieveAPIView
 )
+from django.utils.translation import ugettext as _
 from rest_framework.pagination import LimitOffsetPagination
 
 from common.permissions import IsValidUser, IsOrgAdminOrAppUser
 from common.tree import TreeNodeSerializer
-from common.utils import get_logger
+from common.utils import get_logger, get_object_or_none
 from ..utils import (
-    AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node,
+    AssetPermissionUtil, ParserNode,
 )
+from .. import const
 from ..hands import User, Asset, Node, SystemUser, NodeSerializer
-from .. import serializers, const
-from ..mixins import AssetsFilterMixin
+from .. import serializers
 from ..models import Action
 
+
 logger = get_logger(__name__)
 
 __all__ = [
     'UserGrantedAssetsApi', 'UserGrantedNodesApi',
     'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi',
-    'ValidateUserAssetPermissionApi', 'UserGrantedNodeChildrenApi',
+    'ValidateUserAssetPermissionApi', 'UserGrantedNodesAsTreeApi',
     'UserGrantedNodesWithAssetsAsTreeApi', 'GetUserAssetPermissionActionsApi',
 ]
 
 
 class UserPermissionCacheMixin:
     cache_policy = '0'
-    RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_{}'
+    RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_V2_{}'
     CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
     _object = None
 
@@ -130,12 +134,61 @@ class UserPermissionCacheMixin:
         return resp
 
 
-class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIView):
+class GrantAssetsMixin:
+    serializer_class = serializers.AssetGrantedSerializer
+
+    def get_serializer(self, queryset, many=True):
+        assets_ids = []
+        system_users_ids = set()
+        for asset in queryset:
+            assets_ids.append(asset["id"])
+            system_users_ids.update(set(asset["system_users"]))
+        assets = Asset.objects.filter(id__in=assets_ids).only(
+            *self.serializer_class.Meta.only_fields
+        )
+        assets_map = {asset.id: asset for asset in assets}
+        system_users = SystemUser.objects.filter(id__in=system_users_ids).only(
+            *self.serializer_class.system_users_only_fields
+        )
+        system_users_map = {s.id: s for s in system_users}
+        data = []
+        for item in queryset:
+            i = item["id"]
+            asset = assets_map.get(i)
+            if not asset:
+                continue
+
+            _system_users = item["system_users"]
+            system_users_granted = []
+            for sid, action in _system_users.items():
+                system_user = system_users_map.get(sid)
+                if not system_user:
+                    continue
+                system_user.actions = action
+                system_users_granted.append(system_user)
+            asset.system_users_granted = system_users_granted
+            data.append(asset)
+        return super().get_serializer(data, many=True)
+
+    def search_queryset(self, assets):
+        search = self.request.query_params.get("search")
+        if not search:
+            return assets
+
+        assets_map = {asset['id']: asset for asset in assets}
+        assets_ids = set(assets_map.keys())
+        assets_ids_search = Asset.objects.filter(id__in=assets_ids).filter(
+            Q(hostname__icontains=search) | Q(ip__icontains=search)
+        ).values_list('id', flat=True)
+        assets_ids &= set(assets_ids_search)
+        return [assets_map.get(asset_id) for asset_id in assets_ids]
+
+
+class UserGrantedAssetsApi(UserPermissionCacheMixin, GrantAssetsMixin, ListAPIView):
     """
     用户授权的所有资产
     """
     permission_classes = (IsOrgAdminOrAppUser,)
-    serializer_class = serializers.AssetGrantedSerializer
     pagination_class = LimitOffsetPagination
 
     def get_object(self):
@@ -147,17 +200,10 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV
         return user
 
     def get_queryset(self):
-        queryset = []
         user = self.get_object()
         util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        assets = util.get_assets()
-        for asset, system_users in assets.items():
-            system_users_granted = []
-            for system_user, actions in system_users.items():
-                system_user.actions = actions
-                system_users_granted.append(system_user)
-            asset.system_users_granted = system_users_granted
-            queryset.append(asset)
+        queryset = util.get_assets()
+        queryset = self.search_queryset(queryset)
         return queryset
 
     def get_permissions(self):
@@ -166,12 +212,39 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV
         return super().get_permissions()
 
 
-class UserGrantedNodesApi(UserPermissionCacheMixin, ListAPIView):
+class NodesWithUngroupMixin:
+    util = None
+
+    @staticmethod
+    def get_ungrouped_node(ungroup_key):
+        return Node(key=ungroup_key, id=const.UNGROUPED_NODE_ID,
+                    value=_("ungrouped"))
+
+    @staticmethod
+    def get_empty_node():
+        return Node(key=const.EMPTY_NODE_KEY, id=const.EMPTY_NODE_ID,
+                    value=_("empty"))
+
+    def add_ungrouped_nodes(self, node_map, node_keys):
+        ungroup_key = '1:-1'
+        for key in node_keys:
+            if key.endswith('-1'):
+                ungroup_key = key
+                break
+        ungroup_node = self.get_ungrouped_node(ungroup_key)
+        empty_node = self.get_empty_node()
+        node_map[ungroup_key] = ungroup_node
+        node_map[const.EMPTY_NODE_KEY] = empty_node
+
+
+class UserGrantedNodesApi(UserPermissionCacheMixin, NodesWithUngroupMixin, ListAPIView):
     """
     查询用户授权的所有节点的API
     """
     permission_classes = (IsOrgAdminOrAppUser,)
     serializer_class = NodeSerializer
+    pagination_class = LimitOffsetPagination
+    only_fields = NodeSerializer.Meta.only_fields
 
     def get_object(self):
         user_id = self.kwargs.get('pk', '')
@@ -181,11 +254,31 @@ class UserGrantedNodesApi(UserPermissionCacheMixin, ListAPIView):
             user = self.request.user
         return user
 
+    def get_nodes(self, nodes_with_assets):
+        node_keys = [n["key"] for n in nodes_with_assets]
+        nodes = Node.objects.filter(key__in=node_keys).only(
+            *self.only_fields
+        )
+        nodes_map = {n.key: n for n in nodes}
+        self.add_ungrouped_nodes(nodes_map, node_keys)
+
+        _nodes = []
+        for n in nodes_with_assets:
+            key = n["key"]
+            node = nodes_map.get(key)
+            node._assets_amount = n["assets_amount"]
+            _nodes.append(node)
+        return _nodes
+
+    def get_serializer(self, nodes_with_assets, many=True):
+        nodes = self.get_nodes(nodes_with_assets)
+        return super().get_serializer(nodes, many=True)
+
     def get_queryset(self):
         user = self.get_object()
-        util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        nodes = util.get_nodes()
-        return nodes
+        self.util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
+        nodes_with_assets = self.util.get_nodes_with_assets()
+        return nodes_with_assets
 
     def get_permissions(self):
         if self.kwargs.get('pk') is None:
@@ -193,12 +286,30 @@ class UserGrantedNodesApi(UserPermissionCacheMixin, ListAPIView):
         return super().get_permissions()
 
 
-class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIView):
+class UserGrantedNodesAsTreeApi(UserGrantedNodesApi):
+    serializer_class = TreeNodeSerializer
+    only_fields = ParserNode.nodes_only_fields
+
+    def get_serializer(self, nodes_with_assets, many=True):
+        nodes = self.get_nodes(nodes_with_assets)
+        queryset = []
+        for node in nodes:
+            data = ParserNode.parse_node_to_tree_node(node)
+            queryset.append(data)
+        return self.get_serializer_class()(queryset, many=many)
+
+
+class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, NodesWithUngroupMixin, ListAPIView):
     """
     用户授权的节点并带着节点下资产的api
     """
     permission_classes = (IsOrgAdminOrAppUser,)
     serializer_class = serializers.NodeGrantedSerializer
+    pagination_class = LimitOffsetPagination
+
+    nodes_only_fields = serializers.NodeGrantedSerializer.Meta.only_fields
+    assets_only_fields = serializers.NodeGrantedSerializer.assets_only_fields
+    system_users_only_fields = serializers.NodeGrantedSerializer.system_users_only_fields
 
     def get_object(self):
         user_id = self.kwargs.get('pk', '')
@@ -208,78 +319,127 @@ class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin,
             user = get_object_or_404(User, id=user_id)
         return user
 
-    def get_queryset(self):
+    def get_maps(self, nodes_items):
+        """
+        查库,并加入构造的ungrouped节点
+        :return:
+        ({asset.id: asset}, {node.key: node}, {system_user.id: system_user})
+        """
+        _nodes_keys = set()
+        _assets_ids = set()
+        _system_users_ids = set()
+        for item in nodes_items:
+            _nodes_keys.add(item["key"])
+            _assets_ids.update(set(item["assets"].keys()))
+            for _system_users_id in item["assets"].values():
+                _system_users_ids.update(_system_users_id.keys())
+
+        _nodes = Node.objects.filter(key__in=_nodes_keys).only(
+            *self.nodes_only_fields
+        )
+        _assets = Asset.objects.filter(id__in=_assets_ids).only(
+            *self.assets_only_fields
+        )
+        _system_users = SystemUser.objects.filter(id__in=_system_users_ids).only(
+            *self.system_users_only_fields
+        )
+        _nodes_map = {n.key: n for n in _nodes}
+        self.add_ungrouped_nodes(_nodes_map, _nodes_keys)
+        _assets_map = {a.id: a for a in _assets}
+        _system_users_map = {s.id: s for s in _system_users}
+        return _nodes_map, _assets_map, _system_users_map
+
+    def get_serializer_queryset(self, nodes_items):
+        """
+        将id转为object,同时构造queryset
+        :param nodes_items:
+        [
+            {
+                'key': node.key,
+                'assets_amount': 10
+                'assets': {
+                    asset.id: {
+                        system_user.id: actions,
+                    },
+                },
+            },
+        ]
+        """
         queryset = []
-        user = self.get_object()
-        util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        nodes = util.get_nodes_with_assets()
-        for node, _assets in nodes.items():
-            assets = _assets.keys()
-            for k, v in _assets.items():
-                k.system_users_granted = v
-            node.assets_granted = assets
+        _node_map, _assets_map, _system_users_map = self.get_maps(nodes_items)
+        for item in nodes_items:
+            key = item["key"]
+            node = _node_map.get(key)
+            if not node:
+                continue
+            node._assets_amount = item["assets_amount"]
+            assets_granted = []
+            for asset_id, system_users_ids_action in item["assets"].items():
+                asset = _assets_map.get(asset_id)
+                if not asset:
+                    continue
+                system_user_granted = []
+                for system_user_id, action in system_users_ids_action.items():
+                    system_user = _system_users_map.get(system_user_id)
+                    if not system_user:
+                        continue
+                    system_user.actions = action
+                    system_user_granted.append(system_user)
+                asset.system_users_granted = system_user_granted
+                assets_granted.append(asset)
+            node.assets_granted = assets_granted
             queryset.append(node)
         return queryset
 
-    def sort_assets(self, queryset):
-        for node in queryset:
-            node.assets_granted = super().sort_assets(node.assets_granted)
-        return queryset
-
-    def get_permissions(self):
-        if self.kwargs.get('pk') is None:
-            self.permission_classes = (IsValidUser,)
-        return super().get_permissions()
-
-
-class UserGrantedNodesWithAssetsAsTreeApi(UserPermissionCacheMixin, ListAPIView):
-    serializer_class = TreeNodeSerializer
-    permission_classes = (IsOrgAdminOrAppUser,)
-    show_assets = True
-    system_user_id = None
-
-    def get_permissions(self):
-        if self.kwargs.get('pk') is None:
-            self.permission_classes = (IsValidUser,)
-        return super().get_permissions()
-
-    def get_object(self):
-        user_id = self.kwargs.get('pk', '')
-        if not user_id:
-            user = self.request.user
-        else:
-            user = get_object_or_404(User, id=user_id)
-        return user
+    def get_serializer(self, nodes_items, many=True):
+        queryset = self.get_serializer_queryset(nodes_items)
+        return super().get_serializer(queryset, many=many)
 
     def get_queryset(self):
-        queryset = []
-        self.show_assets = self.request.query_params.get('show_assets', '1') == '1'
-        self.system_user_id = self.request.query_params.get('system_user')
         user = self.get_object()
-        util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        if self.system_user_id:
-            util.filter_permissions(
-                system_users=self.system_user_id
+        self.util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
+        system_user_id = self.request.query_params.get('system_user')
+        if system_user_id:
+            self.util.filter_permissions(
+                system_users=system_user_id
             )
-        nodes = util.get_nodes_with_assets()
-        for node, assets in nodes.items():
-            data = parse_node_to_tree_node(node)
+        nodes_items = self.util.get_nodes_with_assets()
+        return nodes_items
+
+    def get_permissions(self):
+        if self.kwargs.get('pk') is None:
+            self.permission_classes = (IsValidUser,)
+        return super().get_permissions()
+
+
+class UserGrantedNodesWithAssetsAsTreeApi(UserGrantedNodesWithAssetsApi):
+    serializer_class = TreeNodeSerializer
+    permission_classes = (IsOrgAdminOrAppUser,)
+    system_user_id = None
+    nodes_only_fields = ParserNode.nodes_only_fields
+    assets_only_fields = ParserNode.assets_only_fields
+    system_users_only_fields = ParserNode.system_users_only_fields
+
+    def get_serializer(self, nodes_items, many=True):
+        _queryset = super().get_serializer_queryset(nodes_items)
+        queryset = []
+
+        for node in _queryset:
+            data = ParserNode.parse_node_to_tree_node(node)
             queryset.append(data)
-            if not self.show_assets:
-                continue
-            for asset, system_users in assets.items():
-                data = parse_asset_to_tree_node(node, asset, system_users)
+            for asset in node.assets_granted:
+                system_users = asset.system_users_granted
+                data = ParserNode.parse_asset_to_tree_node(node, asset, system_users)
                 queryset.append(data)
         queryset = sorted(queryset)
-        return queryset
+        return self.serializer_class(queryset, many=True)
 
 
-class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIView):
+class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, GrantAssetsMixin, ListAPIView):
     """
     查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产
     """
     permission_classes = (IsOrgAdminOrAppUser,)
-    serializer_class = serializers.AssetGrantedSerializer
     pagination_class = LimitOffsetPagination
 
     def get_object(self):
@@ -291,25 +451,31 @@ class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, List
             user = self.request.user
         return user
 
-    def get_queryset(self):
-        user = self.get_object()
+    def get_node_key(self):
         node_id = self.kwargs.get('node_id')
-        util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        nodes = util.get_nodes_with_assets()
         if str(node_id) == const.UNGROUPED_NODE_ID:
-            node = util.tree.ungrouped_node
+            key = self.util.tree.ungrouped_key
         elif str(node_id) == const.EMPTY_NODE_ID:
-            node = util.tree.empty_node
+            key = const.EMPTY_NODE_KEY
         else:
             node = get_object_or_404(Node, id=node_id)
-        if node == util.tree.root_node:
-            assets = util.get_assets()
-        else:
-            assets = nodes.get(node, {})
-        for asset, system_users in assets.items():
-            asset.system_users_granted = system_users
+            key = node.key
+        return key
 
-        assets = list(assets.keys())
+    def get_queryset(self):
+        user = self.get_object()
+        self.util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
+        key = self.get_node_key()
+        nodes_items = self.util.get_nodes_with_assets()
+        assets_system_users = {}
+        for item in nodes_items:
+            if item["key"] == key:
+                assets_system_users = item["assets"]
+                break
+        assets = []
+        for asset_id, system_users in assets_system_users.items():
+            assets.append({"id": asset_id, "system_users": system_users})
+        assets = self.search_queryset(assets)
         return assets
 
     def get_permissions(self):
@@ -318,92 +484,6 @@ class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, List
         return super().get_permissions()
 
 
-class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView):
-    """
-    获取用户自己授权节点下子节点的api
-    """
-    permission_classes = (IsValidUser,)
-    serializer_class = serializers.AssetPermissionNodeSerializer
-
-    def get_object(self):
-        return self.request.user
-
-    def get_children_queryset(self):
-        user = self.get_object()
-        util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        node_id = self.request.query_params.get('id')
-        nodes_granted = util.get_nodes_with_assets()
-        if not nodes_granted:
-            return []
-        root_nodes = [node for node in nodes_granted.keys() if node.is_root()]
-
-        queryset = []
-        if node_id and node_id in [str(node.id) for node in nodes_granted]:
-            node = [node for node in nodes_granted if str(node.id) == node_id][0]
-        elif len(root_nodes) == 1:
-            node = root_nodes[0]
-            node.assets_amount = len(nodes_granted[node])
-            queryset.append(node)
-        else:
-            for node in root_nodes:
-                node.assets_amount = len(nodes_granted[node])
-                queryset.append(node)
-            return queryset
-
-        children = []
-        for child in node.get_children():
-            if child in nodes_granted:
-                child.assets_amount = len(nodes_granted[node])
-                children.append(child)
-        children = sorted(children, key=lambda x: x.value)
-        queryset.extend(children)
-        fake_nodes = []
-        for asset, system_users in nodes_granted[node].items():
-            fake_node = asset.as_node()
-            fake_node.assets_amount = 0
-            system_users = [s for s in system_users if asset.has_protocol(s.protocol)]
-            fake_node.asset.system_users_granted = system_users
-            fake_node.key = node.key + ':0'
-            fake_nodes.append(fake_node)
-        fake_nodes = sorted(fake_nodes, key=lambda x: x.value)
-        queryset.extend(fake_nodes)
-        return queryset
-
-    def get_search_queryset(self, keyword):
-        user = self.get_object()
-        util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        nodes_granted = util.get_nodes_with_assets()
-        queryset = []
-        for node, assets in nodes_granted.items():
-            matched_assets = []
-            node_matched = node.value.lower().find(keyword.lower()) >= 0
-            asset_has_matched = False
-            for asset, system_users in assets.items():
-                asset_matched = (asset.hostname.lower().find(keyword.lower()) >= 0) \
-                                or (asset.ip.find(keyword.lower()) >= 0)
-                if node_matched or asset_matched:
-                    asset_has_matched = True
-                    fake_node = asset.as_node()
-                    fake_node.assets_amount = 0
-                    system_users = [s for s in system_users if
-                                    asset.has_protocol(s.protocol)]
-                    fake_node.asset.system_users_granted = system_users
-                    fake_node.key = node.key + ':0'
-                    matched_assets.append(fake_node)
-            if asset_has_matched:
-                node.assets_amount = len(matched_assets)
-                queryset.append(node)
-                queryset.extend(sorted(matched_assets, key=lambda x: x.value))
-        return queryset
-
-    def get_queryset(self):
-        keyword = self.request.query_params.get('search')
-        if keyword:
-            return self.get_search_queryset(keyword)
-        else:
-            return self.get_children_queryset()
-
-
 class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView):
     permission_classes = (IsOrgAdminOrAppUser,)
     
@@ -412,24 +492,24 @@ class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView):
         asset_id = request.query_params.get('asset_id', '')
         system_id = request.query_params.get('system_user_id', '')
         action_name = request.query_params.get('action_name', '')
+        cache_policy = self.request.query_params.get("cache_policy", '0')
+
+        try:
+            asset_id = uuid.UUID(asset_id)
+            system_id = uuid.UUID(system_id)
+        except ValueError:
+            return Response({'msg': False}, status=403)
 
         user = get_object_or_404(User, id=user_id)
-        asset = get_object_or_404(Asset, id=asset_id)
-        su = get_object_or_404(SystemUser, id=system_id)
-
-        util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
-        granted_assets = util.get_assets()
-        granted_system_users = granted_assets.get(asset, {})
-
-        if su not in granted_system_users:
-            return Response({'msg': False}, status=403)
-
-        action = granted_system_users[su]
-        choices = Action.value_to_choices(action)
-        if action_name not in choices:
-            return Response({'msg': False}, status=403)
-
-        return Response({'msg': True}, status=200)
+        util = AssetPermissionUtil(user, cache_policy=cache_policy)
+        assets = util.get_assets()
+        for asset in assets:
+            if asset_id == asset["id"]:
+                action = asset["system_users"].get(system_id)
+                if action and action_name in Action.value_to_choices(action):
+                    return Response({'msg': True}, status=200)
+                break
+        return Response({'msg': False}, status=403)
 
 
 class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, RetrieveAPIView):
diff --git a/apps/perms/api/user_remote_app_permission.py b/apps/perms/api/user_remote_app_permission.py
index 4cbfcd9b4..5c816cffb 100644
--- a/apps/perms/api/user_remote_app_permission.py
+++ b/apps/perms/api/user_remote_app_permission.py
@@ -13,13 +13,13 @@ from ..utils import (
     RemoteAppPermissionUtil, construct_remote_apps_tree_root,
     parse_remote_app_to_tree_node,
 )
-from ..hands import User, RemoteApp, RemoteAppSerializer
+from ..hands import User, RemoteApp, RemoteAppSerializer, UserGroup
 from ..mixins import RemoteAppFilterMixin
 
 
 __all__ = [
     'UserGrantedRemoteAppsApi', 'ValidateUserRemoteAppPermissionApi',
-    'UserGrantedRemoteAppsAsTreeApi',
+    'UserGrantedRemoteAppsAsTreeApi', 'UserGroupGrantedRemoteAppsApi',
 ]
 
 
@@ -94,3 +94,20 @@ class ValidateUserRemoteAppPermissionApi(APIView):
         if remote_app not in remote_apps:
             return Response({'msg': False}, status=403)
         return Response({'msg': True}, status=200)
+
+
+# RemoteApp permission
+
+class UserGroupGrantedRemoteAppsApi(ListAPIView):
+    permission_classes = (IsOrgAdminOrAppUser, )
+    serializer_class = RemoteAppSerializer
+
+    def get_queryset(self):
+        queryset = []
+        user_group_id = self.kwargs.get('pk')
+        if not user_group_id:
+            return queryset
+        user_group = get_object_or_404(UserGroup, id=user_group_id)
+        util = RemoteAppPermissionUtil(user_group)
+        queryset = util.get_remote_apps()
+        return queryset
diff --git a/apps/perms/const.py b/apps/perms/const.py
index b18580747..4ccab5e38 100644
--- a/apps/perms/const.py
+++ b/apps/perms/const.py
@@ -3,3 +3,4 @@
 
 UNGROUPED_NODE_ID = "00000000-0000-0000-0000-000000000002"
 EMPTY_NODE_ID = "00000000-0000-0000-0000-000000000003"
+EMPTY_NODE_KEY = "1:-2"
diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py
index d5fafc63a..cb3e37768 100644
--- a/apps/perms/models/asset_permission.py
+++ b/apps/perms/models/asset_permission.py
@@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
 
 from common.utils import date_expired_default, set_or_append_attr_bulk
 from orgs.mixins import OrgModelMixin
+from assets.models import Asset, SystemUser, Node
 
 from .base import BasePermission
 
@@ -85,7 +86,11 @@ class AssetPermission(BasePermission):
 
     @classmethod
     def get_queryset_with_prefetch(cls):
-        return cls.objects.all().valid().prefetch_related('nodes', 'assets', 'system_users')
+        return cls.objects.all().valid().prefetch_related(
+            models.Prefetch('nodes', queryset=Node.objects.all().only('key')),
+            models.Prefetch('assets', queryset=Asset.objects.all().only('id')),
+            models.Prefetch('system_users', queryset=SystemUser.objects.all().only('id'))
+        )
 
     def get_all_assets(self):
         assets = set(self.assets.all())
diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py
index 639ba7140..bdf2e1fdc 100644
--- a/apps/perms/serializers/user_permission.py
+++ b/apps/perms/serializers/user_permission.py
@@ -2,16 +2,16 @@
 #
 
 from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
 
-from assets.models import Node, SystemUser
-from assets.serializers import AssetSerializer
-
+from assets.models import Node, SystemUser, Asset
+from assets.serializers import ProtocolsField
 from .asset_permission import ActionsField
 
 __all__ = [
-    'AssetPermissionNodeSerializer', 'GrantedNodeSerializer',
+    'GrantedNodeSerializer',
     'NodeGrantedSerializer', 'AssetGrantedSerializer',
-    'ActionsSerializer',
+    'ActionsSerializer', 'AssetSystemUserSerializer',
 ]
 
 
@@ -23,87 +23,56 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = SystemUser
-        fields = (
-            'id', 'name', 'username', 'priority', "actions",
+        only_fields = (
+            'id', 'name', 'username', 'priority',
             'protocol', 'login_mode',
         )
+        fields = list(only_fields) + ["actions"]
+        read_only_fields = fields
 
 
-class AssetGrantedSerializer(AssetSerializer):
+class AssetGrantedSerializer(serializers.ModelSerializer):
     """
     被授权资产的数据结构
     """
+    protocols = ProtocolsField(label=_('Protocols'), required=False, read_only=True)
     system_users_granted = AssetSystemUserSerializer(many=True, read_only=True)
     system_users_join = serializers.SerializerMethodField()
+    system_users_only_fields = AssetSystemUserSerializer.Meta.only_fields
+
+    class Meta:
+        model = Asset
+        only_fields = [
+            "id", "hostname", "ip", "protocols", "os", 'domain',
+            "platform", "org_id",
+        ]
+        fields = only_fields + ['system_users_granted', 'system_users_join', "org_name"]
+        read_only_fields = fields
 
     @staticmethod
     def get_system_users_join(obj):
         system_users = [s.username for s in obj.system_users_granted]
         return ', '.join(system_users)
 
-    def get_field_names(self, declared_fields, info):
-        fields = (
-            "id", "hostname", "ip", "protocols",
-            "system_users_granted", "is_active", "system_users_join", "os",
-            'domain', "platform", "comment", "org_id", "org_name",
-        )
-        return fields
-
-
-class AssetPermissionNodeSerializer(serializers.ModelSerializer):
-    asset = AssetGrantedSerializer(required=False)
-    assets_amount = serializers.SerializerMethodField()
-
-    tree_id = serializers.SerializerMethodField()
-    tree_parent = serializers.SerializerMethodField()
-
-    class Meta:
-        model = Node
-        fields = [
-            'id', 'key', 'value', 'asset', 'is_node', 'org_id',
-            'tree_id', 'tree_parent', 'assets_amount',
-        ]
-
-    @staticmethod
-    def get_assets_amount(obj):
-        return obj.assets_amount
-
-    @staticmethod
-    def get_tree_id(obj):
-        return obj.key
-
-    @staticmethod
-    def get_tree_parent(obj):
-        return obj.parent_key
-
 
 class NodeGrantedSerializer(serializers.ModelSerializer):
     """
     授权资产组
     """
     assets_granted = AssetGrantedSerializer(many=True, read_only=True)
-    assets_amount = serializers.SerializerMethodField()
-    parent = serializers.SerializerMethodField()
-    name = serializers.SerializerMethodField()
+    assets_amount = serializers.ReadOnlyField()
+    name = serializers.ReadOnlyField(source='value')
+
+    assets_only_fields = AssetGrantedSerializer.Meta.only_fields
+    system_users_only_fields = AssetGrantedSerializer.system_users_only_fields
 
     class Meta:
         model = Node
-        fields = [
-            'id', 'key', 'name', 'value', 'parent',
-            'assets_granted', 'assets_amount', 'org_id',
+        only_fields = ['id', 'key', 'value', "org_id"]
+        fields = only_fields + [
+            'name', 'assets_granted', 'assets_amount',
         ]
-
-    @staticmethod
-    def get_assets_amount(obj):
-        return len(obj.assets_granted)
-
-    @staticmethod
-    def get_name(obj):
-        return obj.name
-
-    @staticmethod
-    def get_parent(obj):
-        return obj.parent.id
+        read_only_fields = fields
 
 
 class GrantedNodeSerializer(serializers.ModelSerializer):
@@ -112,6 +81,7 @@ class GrantedNodeSerializer(serializers.ModelSerializer):
         fields = [
             'id', 'name', 'key', 'value',
         ]
+        read_only_fields = fields
 
 
 class ActionsSerializer(serializers.Serializer):
diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py
index 611661c5d..459426f06 100644
--- a/apps/perms/urls/api_urls.py
+++ b/apps/perms/urls/api_urls.py
@@ -1,7 +1,8 @@
 # coding:utf-8
 
-from django.urls import path
+from django.urls import path, re_path
 from rest_framework import routers
+from common import api as capi
 from .. import api
 
 app_name = 'perms'
@@ -12,26 +13,36 @@ router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remot
 
 
 asset_permission_urlpatterns = [
-    # 查询某个用户授权的资产和资产组
-    path('user/<uuid:pk>/assets/', api.UserGrantedAssetsApi.as_view()),
+    # Assets
     path('users/<uuid:pk>/assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'),
-    path('user/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'),
-    path('user/<uuid:pk>/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
-    path('user/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'),
-    path('user/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(), name='my-node-children'),
-    path('user/<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
-    path('user/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
-    path('user/<uuid:pk>/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'),
-    path('user/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='my-nodes-assets'),
-    path('user/<uuid:pk>/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'),
-    path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-assets-as-tree'),
+    path('users/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'),
+
+    # Node as tree
+    path('users/<uuid:pk>/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'),
+    path('users/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'),
+
+    # Nodes
+    path('users/<uuid:pk>/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
+    path('users/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'),
+
+    # Node assets
+    path('users/<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
+    path('users/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
+
+    # Node with assets
+    path('users/<uuid:pk>/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'),
+    path('users/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='my-nodes-assets'),
+
+    # Node assets as tree
+    path('users/<uuid:pk>/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'),
+    path('users/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-assets-as-tree'),
 
     # 查询某个用户组授权的资产和资产组
-    path('user-group/<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
-    path('user-group/<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
-    path('user-group/<uuid:pk>/nodes-assets/', api.UserGroupGrantedNodesWithAssetsApi.as_view(), name='user-group-nodes-assets'),
-    path('user-group/<uuid:pk>/nodes-assets/tree/', api.UserGroupGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-group-nodes-assets-as-tree'),
-    path('user-group/<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'),
+    path('user-groups/<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
+    path('user-groups/<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
+    path('user-groups/<uuid:pk>/nodes-assets/', api.UserGroupGrantedNodesWithAssetsApi.as_view(), name='user-group-nodes-assets'),
+    path('user-groups/<uuid:pk>/nodes-assets/tree/', api.UserGroupGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-group-nodes-assets-as-tree'),
+    path('user-groups/<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'),
 
     # 用户和资产授权变更
     path('asset-permissions/<uuid:pk>/user/remove/', api.AssetPermissionRemoveUserApi.as_view(), name='asset-permission-remove-user'),
@@ -40,25 +51,25 @@ asset_permission_urlpatterns = [
     path('asset-permissions/<uuid:pk>/asset/add/', api.AssetPermissionAddAssetApi.as_view(), name='asset-permission-add-asset'),
 
     # 验证用户是否有某个资产和系统用户的权限
-    path('asset-permission/user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'),
-    path('asset-permission/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'),
+    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'),
 ]
 
 
 remote_app_permission_urlpatterns = [
     # 查询用户授权的RemoteApp
-    path('user/<uuid:pk>/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'),
-    path('user/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'),
+    path('users/<uuid:pk>/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'),
+    path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'),
 
     # 获取用户授权的RemoteApp树
-    path('user/<uuid:pk>/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'),
-    path('user/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'),
+    path('users/<uuid:pk>/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-group/<uuid:pk>/remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'),
+    path('user-groups/<uuid:pk>/remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'),
 
     # 校验用户对RemoteApp的权限
-    path('remote-app-permission/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'),
+    path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'),
 
     # 用户和RemoteApp变更
     path('remote-app-permissions/<uuid:pk>/user/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'),
@@ -67,7 +78,11 @@ remote_app_permission_urlpatterns = [
     path('remote-app-permissions/<uuid:pk>/remote-app/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'),
 ]
 
-urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns
+old_version_urlpatterns = [
+    re_path('(?P<resource>user|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
 
diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py
index 3a8c2d387..c437753b6 100644
--- a/apps/perms/utils/asset_permission.py
+++ b/apps/perms/utils/asset_permission.py
@@ -1,80 +1,65 @@
 # coding: utf-8
 
+import time
 import uuid
 from collections import defaultdict
 import json
 from hashlib import md5
-import time
 import itertools
 
 from django.utils import timezone
 from django.db.models import Q
 from django.core.cache import cache
 from django.conf import settings
-from django.utils.translation import ugettext as _
 
 from orgs.utils import set_to_root_org
-from common.utils import get_logger
+from common.utils import get_logger, timeit
 from common.tree import TreeNode
+from assets.utils import NodeUtil
 from .. import const
 from ..models import AssetPermission, Action
 from ..hands import Node, Asset
-from assets.utils import NodeUtil
+from .stack import PermSystemUserNodeUtil, PermAssetsAmountUtil
 
 logger = get_logger(__file__)
 
 
 __all__ = [
     'AssetPermissionUtil', 'is_obj_attr_has', 'sort_assets',
-    'parse_asset_to_tree_node', 'parse_node_to_tree_node',
+    'ParserNode',
 ]
 
 
-class TreeNodeCounter(NodeUtil):
-    def __init__(self, nodes):
-        self.__nodes = nodes
-        super().__init__(with_assets_amount=True)
-
-    def get_queryset(self):
-        return self.__nodes
-
-
-def timeit(func):
-    def wrapper(*args, **kwargs):
-        logger.debug("Start call: {}".format(func.__name__))
-        now = time.time()
-        result = func(*args, **kwargs)
-        using = time.time() - now
-        logger.debug("Call {} end, using: {:.2}s".format(func.__name__, using))
-        return result
-    return wrapper
-
-
 class GenerateTree:
     def __init__(self):
         """
         nodes = {
-          "<node1>": {
+          node.key: {
             "system_users": {
-              "system_user": action,
-              "system_user2": action,
+              system_user.id: actions,
             },
-            "assets": set([<asset_instance>]),
-          }
+            "assets": set([asset.id,]),
+          },
         }
         assets = {
-           "<asset_instance2>": {
-             "system_user": action,
-             "system_user2": action,
+           asset.id: {
+             system_user.id: actions,
            },
         }
         """
         self._node_util = None
-        self.nodes = defaultdict(lambda: {"system_users": defaultdict(int), "assets": set(), "assets_amount": 0})
+        self.nodes = defaultdict(lambda: {
+            "system_users": defaultdict(int), "assets": set(),
+            "assets_amount": 0, "all_assets": set(),
+        })
         self.assets = defaultdict(lambda: defaultdict(int))
         self._root_node = None
         self._ungroup_node = None
         self._nodes_with_assets = None
+        self._all_assets_nodes_key = None
+        self._asset_counter = 0
+        self._system_user_counter = 0
+        self._nodes_assets_counter = 0
 
     @property
     def node_util(self):
@@ -82,115 +67,160 @@ class GenerateTree:
             self._node_util = NodeUtil()
         return self._node_util
 
+    @staticmethod
+    def key_sort(key):
+        key_list = [int(i) for i in key.split(':')]
+        return len(key_list), key_list
+
     @property
-    def root_node(self):
+    def root_key(self):
         if self._root_node:
             return self._root_node
-        all_nodes = self.nodes.keys()
+        all_keys = self.nodes.keys()
         # 如果没有授权节点,就放到默认的根节点下
-        if not all_nodes:
+        if not all_keys:
             return None
-        root_node = min(all_nodes)
-        self._root_node = root_node
-        return root_node
+        root_key = min(all_keys, key=self.key_sort)
+        self._root_key = root_key
+        return root_key
 
     @property
-    def ungrouped_node(self):
+    def all_assets_nodes_keys(self):
+        if not self._all_assets_nodes_key:
+            self._all_assets_nodes_key = Asset.get_all_nodes_keys()
+        return self._all_assets_nodes_key
+
+    @property
+    def ungrouped_key(self):
         if self._ungroup_node:
             return self._ungroup_node
-        node_id = const.UNGROUPED_NODE_ID
-        if self.root_node:
-            node_key = "{}:{}".format(self.root_node.key, self.root_node.child_mark)
+        if self.root_key:
+            node_key = "{}:{}".format(self.root_key, '-1')
         else:
-            node_key = '0:0'
-        node_value = _("Default")
-        node = Node(id=node_id, key=node_key, value=node_value)
-        self.add_node(node, {})
-        self._ungroup_node = node
-        return node
+            node_key = '1:-1'
+        self._ungroup_node = node_key
+        return node_key
 
-    @property
-    def empty_node(self):
-        node_id = const.EMPTY_NODE_ID
-        value = _('Empty')
-        node = Node(id=node_id, value=value)
-        return node
+    @timeit
+    def add_assets_without_system_users(self, assets_ids):
+        for asset_id in assets_ids:
+            self.add_asset(asset_id, {})
 
-    #@timeit
-    def add_assets_without_system_users(self, assets):
-        for asset in assets:
-            self.add_asset(asset, {})
+    @timeit
+    def add_assets(self, assets_ids_with_system_users):
+        for asset_id, system_users_ids in assets_ids_with_system_users.items():
+            self.add_asset(asset_id, system_users_ids)
 
-    #@timeit
-    def add_assets(self, assets):
-        for asset, system_users in assets.items():
-            self.add_asset(asset, system_users)
+    # @timeit
+    def add_asset(self, asset_id, system_users_ids=None):
+        """
+        :param asset_id:
+        :param system_users_ids: {system_user.id: actions, }
+        :return:
+        """
+        if not system_users_ids:
+            system_users_ids = defaultdict(int)
 
-    # #@timeit
-    def add_asset(self, asset, system_users=None):
-        nodes = asset.nodes.all()
-        nodes = self.node_util.get_nodes_by_queryset(nodes)
-        if not system_users:
-            system_users = defaultdict(int)
-        else:
-            system_users = {k: v for k, v in system_users.items()}
-        _system_users = self.assets[asset]
-        for system_user, action in _system_users.items():
-            system_users[system_user] |= action
+        # 获取已有资产的系统用户和actions,并更新到最新系统用户信息中
+        old_system_users_ids = self.assets[asset_id]
+        for system_user_id, action in old_system_users_ids.items():
+            system_users_ids[system_user_id] |= action
 
-        # 获取父节点们
-        parents = self.node_util.get_nodes_parents(nodes, with_self=True)
-        for node in parents:
-            _system_users = self.nodes[node]["system_users"]
-            self.nodes[node]["assets_amount"] += 1
-            for system_user, action in _system_users.items():
-                system_users[system_user] |= action
-
-        # 过滤系统用户的协议
-        system_users = {s: v for s, v in system_users.items() if asset.has_protocol(s.protocol)}
-        self.assets[asset] = system_users
-
-        in_nodes = set(self.nodes.keys()) & set(nodes)
+        asset_nodes_keys = self.all_assets_nodes_keys.get(asset_id, [])
+        # {asset.id: [node.key, ], }
+        # 获取用户在的节点
+        in_nodes = set(self.nodes.keys()) & set(asset_nodes_keys)
         if not in_nodes:
-            self.nodes[self.ungrouped_node]["assets_amount"] += 1
-            self.nodes[self.ungrouped_node]["assets"].add(system_users)
+            self.nodes[self.ungrouped_key]["assets"].add(asset_id)
+            self.assets[asset_id] = system_users_ids
             return
 
-        for node in in_nodes:
-            self.nodes[node]["assets"].add(asset)
+        # 遍历用户应该在的节点
+        for key in in_nodes:
+            # 把自己加入到树上的节点中
+            self.nodes[key]["assets"].add(asset_id)
+            # 获取自己所在节点的系统用户,并添加进去
+            node_system_users_ids = self.nodes[key]["system_users"]
+            for system_user_id, action in node_system_users_ids.items():
+                system_users_ids[system_user_id] |= action
+        self.assets[asset_id] = system_users_ids
 
-    def add_node(self, node, system_users=None):
-        if not system_users:
-            system_users = defaultdict(int)
-        self.nodes[node]["system_users"] = system_users
+    def add_node(self, node_key, system_users_ids=None):
+        """
+        :param node_key: node.key
+        :param system_users_ids: {system_user.id: actions,}
+        :return:
+        """
+        if not system_users_ids:
+            system_users_ids = defaultdict(int)
+        self.nodes[node_key]["system_users"] = system_users_ids
 
     # 添加树节点
-    #@timeit
-    def add_nodes(self, nodes):
-        _nodes = nodes.keys()
-        family = self.node_util.get_family(_nodes, with_children=True)
-        for node in family:
-            self.add_node(node, nodes.get(node, {}))
+    @timeit
+    def add_nodes(self, nodes_keys_with_system_users_ids):
+        """
+        :param nodes_keys_with_system_users_ids:
+        {node.key: {system_user.id: actions,}, }
+        :return:
+        """
+        util = PermSystemUserNodeUtil()
+        family = util.get_nodes_family_and_system_users(nodes_keys_with_system_users_ids)
+        for key, system_users in family.items():
+            self.add_node(key, system_users)
 
     def get_assets(self):
-        return dict(self.assets)
+        """
+        :return:
+        [
+            {"id": asset.id, "system_users": {system_user.id: actions, }},
+        ]
+        """
+        assets = []
+        for asset_id, system_users in self.assets.items():
+            assets.append({"id": asset_id, "system_users": system_users})
+        return assets
 
-    #@timeit
+    @timeit
     def get_nodes_with_assets(self):
+        """
+        :return:
+        [
+            {
+                'key': node.key,
+                'assets_amount': 10
+                'assets': {
+                    asset.id: {
+                        system_user.id: actions,
+                    },
+                },
+            },
+        ]
+        """
         if self._nodes_with_assets:
             return self._nodes_with_assets
-        nodes = {}
-        for node, values in self.nodes.items():
-            node._assets_amount = values["assets_amount"]
-            nodes[node] = {asset: self.assets.get(asset, {}) for asset in values["assets"]}
+        util = PermAssetsAmountUtil()
+        nodes_with_assets_amount = util.compute_nodes_assets_amount(self.nodes)
+        nodes = []
+        for key, values in nodes_with_assets_amount.items():
+            assets = {asset_id: self.assets.get(asset_id) for asset_id in values["assets"]}
+            nodes.append({
+                "key": key, "assets": assets,
+                "assets_amount": values["assets_amount"]
+            })
         # 如果返回空节点,页面构造授权资产树报错
         if not nodes:
-            nodes[self.empty_node] = {}
+            nodes.append({
+                "key": const.EMPTY_NODE_KEY, "assets": {}, "assets_amount": 0
+            })
+        nodes.sort(key=lambda n: self.key_sort(n["key"]))
         self._nodes_with_assets = nodes
-        return dict(nodes)
+        return nodes
 
     def get_nodes(self):
-        return list(self.nodes.keys())
+        nodes = list(self.nodes.keys())
+        if not nodes:
+            nodes.append(const.EMPTY_NODE_KEY)
+        return list(nodes)
 
 
 def get_user_permissions(user, include_group=True):
@@ -228,8 +258,8 @@ def get_system_user_permissions(system_user):
 
 
 class AssetPermissionCacheMixin:
-    CACHE_KEY_PREFIX = '_ASSET_PERM_CACHE_'
-    CACHE_META_KEY_PREFIX = '_ASSET_PERM_META_KEY_'
+    CACHE_KEY_PREFIX = '_ASSET_PERM_CACHE_V2_'
+    CACHE_META_KEY_PREFIX = '_ASSET_PERM_META_KEY_V2_'
     CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
     CACHE_POLICY_MAP = (('0', 'never'), ('1', 'using'), ('2', 'refresh'))
     cache_policy = '1'
@@ -283,6 +313,7 @@ class AssetPermissionCacheMixin:
         return self.get_cache_key('SYSTEM_USER')
 
     def get_resource_from_cache(self, resource):
+        logger.debug("Try get resource from cache")
         key_map = {
             "assets": self.asset_key,
             "nodes": self.node_key,
@@ -294,18 +325,22 @@ class AssetPermissionCacheMixin:
             raise ValueError("Not a valid resource: {}".format(resource))
         cached = cache.get(key)
         if not cached:
+            logger.debug("Not found resource cache, update it")
             self.update_cache()
             cached = cache.get(key)
         return cached
 
     def get_resource(self, resource):
         if self._is_using_cache():
+            logger.debug("Using cache to get resource")
             return self.get_resource_from_cache(resource)
         elif self._is_refresh_cache():
+            logger.debug("Need refresh cache")
             self.expire_cache()
             data = self.get_resource_from_cache(resource)
             return data
         else:
+            logger.debug("Not using cache get source")
             return self.get_resource_without_cache(resource)
 
     def get_resource_without_cache(self, resource):
@@ -430,88 +465,91 @@ class AssetPermissionUtil(AssetPermissionCacheMixin):
         self._permissions = permissions
         return permissions
 
-    #@timeit
+    @timeit
     def filter_permissions(self, **filters):
         filters_json = json.dumps(filters, sort_keys=True)
         self._permissions = self.permissions.filter(**filters)
         self._filter_id = md5(filters_json.encode()).hexdigest()
 
-    #@timeit
+    @timeit
     def get_nodes_direct(self):
         """
-        返回用户/组授权规则直接关联的节点
-        :return: {node1: {system_user1: {'actions': set()},}}
+        返回直接授权的节点,
+        并将节点添加到tree.nodes中,并将节点下的资产添加到tree.assets中
+        :return:
+        {node.key: {system_user.id: actions,}, }
         """
         if self._nodes_direct:
             return self._nodes_direct
-        nodes = defaultdict(lambda: defaultdict(int))
+        nodes_keys = defaultdict(lambda: defaultdict(int))
         for perm in self.permissions:
             actions = [perm.actions]
-            system_users = perm.system_users.all()
-            _nodes = perm.nodes.all()
-            for node, system_user, action in itertools.product(_nodes, system_users, actions):
-                nodes[node][system_user] |= action
-        self.tree.add_nodes(nodes)
-        self._nodes_direct = nodes
-        return nodes
+            system_users_ids = [s.id for s in perm.system_users.all()]
+            _nodes_keys = [n.key for n in perm.nodes.all()]
+            iterable = itertools.product(_nodes_keys, system_users_ids, actions)
+            for node_key, sys_id, action in iterable:
+                nodes_keys[node_key][sys_id] |= action
+
+        self.tree.add_nodes(nodes_keys)
+
+        pattern = set()
+        for key in nodes_keys:
+            pattern.add(r'^{0}$|^{0}:'.format(key))
+        pattern = '|'.join(list(pattern))
+        if pattern:
+            assets_ids = Asset.objects.filter(
+                nodes__key__regex=pattern
+            ).values_list("id", flat=True).distinct()
+        else:
+            assets_ids = []
+        self.tree.add_assets_without_system_users(assets_ids)
+        self._nodes_direct = nodes_keys
+        return nodes_keys
 
     def get_nodes_without_cache(self):
-        self.get_assets_direct()
+        self.get_assets_without_cache()
         return self.tree.get_nodes()
 
-    #@timeit
+    @timeit
     def get_assets_direct(self):
         """
-        返回用户授权规则直接关联的资产
-        :return: {asset1: {system_user1: 1,}}
+        返回直接授权的资产,
+        并添加到tree.assets中
+        :return:
+        {asset.id: {system_user.id: actions, }, }
         """
         if self._assets_direct:
             return self._assets_direct
-        assets = defaultdict(lambda: defaultdict(int))
+        assets_ids = defaultdict(lambda: defaultdict(int))
         for perm in self.permissions:
             actions = [perm.actions]
-            _assets = perm.assets.valid().only(*self.assets_only)
-            system_users = perm.system_users.all()
-            iterable = itertools.product(_assets, system_users, actions)
-            for asset, system_user, action in iterable:
-                assets[asset][system_user] |= action
-        self.tree.add_assets(assets)
-        self._assets_direct = assets
-        return assets
+            _assets_ids = [a.id for a in perm.assets.all()]
+            system_users_ids = [s.id for s in perm.system_users.all()]
+            iterable = itertools.product(_assets_ids, system_users_ids, actions)
+            for asset_id, sys_id, action in iterable:
+                assets_ids[asset_id][sys_id] |= action
+        self.tree.add_assets(assets_ids)
+        self._assets_direct = assets_ids
+        return assets_ids
 
-    #@timeit
+    @timeit
     def get_assets_without_cache(self):
         """
-        :return: {asset1: set(system_user1,)}
+        :return:
+        [
+            {"id": asset.id, "system_users": {system_user.id: actions, }},
+        ]
         """
         if self._assets:
             return self._assets
+        self.get_nodes_direct()
         self.get_assets_direct()
-        nodes = self.get_nodes_direct()
-        pattern = set()
-        for node in nodes:
-            pattern.add(r'^{0}$|^{0}:'.format(node.key))
-        pattern = '|'.join(list(pattern))
-        if pattern:
-            assets = Asset.objects.filter(nodes__key__regex=pattern).valid() \
-                .prefetch_related('nodes')\
-                .only(*self.assets_only)\
-                .distinct()
-        else:
-            assets = []
-        assets = list(assets)
-        self.tree.add_assets_without_system_users(assets)
         assets = self.tree.get_assets()
         self._assets = assets
         return assets
 
-    #@timeit
+    @timeit
     def get_nodes_with_assets_without_cache(self):
-        """
-        返回节点并且包含资产
-        {"node": {"asset": {"system_user": 1})}}
-        :return:
-        """
         self.get_assets_without_cache()
         nodes_assets = self.tree.get_nodes_with_assets()
         return nodes_assets
@@ -545,67 +583,72 @@ def sort_assets(assets, order_by='hostname', reverse=False):
     return assets
 
 
-def parse_node_to_tree_node(node):
-    name = '{} ({})'.format(node.value, node.assets_amount)
-    data = {
-        'id': node.key,
-        'name': name,
-        'title': name,
-        'pId': node.parent_key,
-        'isParent': True,
-        'open': node.is_root(),
-        'meta': {
-            'node': {
-                "id": node.id,
-                "key": node.key,
-                "value": node.value,
-            },
-            'type': 'node'
-        }
-    }
-    tree_node = TreeNode(**data)
-    return tree_node
+class ParserNode:
+    nodes_only_fields = ("key", "value", "id")
+    assets_only_fields = ("platform", "hostname", "id", "ip", "protocols")
+    system_users_only_fields = (
+        "id", "name", "username", "protocol", "priority", "login_mode",
+    )
 
-
-def parse_asset_to_tree_node(node, asset, system_users):
-    icon_skin = 'file'
-    if asset.platform.lower() == 'windows':
-        icon_skin = 'windows'
-    elif asset.platform.lower() == 'linux':
-        icon_skin = 'linux'
-    _system_users = []
-    for system_user, action in system_users.items():
-        _system_users.append({
-            'id': system_user.id,
-            'name': system_user.name,
-            'username': system_user.username,
-            'protocol': system_user.protocol,
-            'priority': system_user.priority,
-            'login_mode': system_user.login_mode,
-            'actions': [Action.value_to_choices(action)],
-        })
-    data = {
-        'id': str(asset.id),
-        'name': asset.hostname,
-        'title': asset.ip,
-        'pId': node.key,
-        'isParent': False,
-        'open': False,
-        'iconSkin': icon_skin,
-        'meta': {
-            'system_users': _system_users,
-            'type': 'asset',
-            'asset': {
-                'id': asset.id,
-                'hostname': asset.hostname,
-                'ip': asset.ip,
-                'protocols': asset.protocols_as_list,
-                'platform': asset.platform,
-                'domain': None if not asset.domain else asset.domain.id,
-                'is_active': asset.is_active,
-                'comment': asset.comment
-            },
+    @staticmethod
+    def parse_node_to_tree_node(node):
+        name = '{} ({})'.format(node.value, node.assets_amount)
+        data = {
+            'id': node.key,
+            'name': name,
+            'title': name,
+            'pId': node.parent_key,
+            'isParent': True,
+            'open': node.is_root(),
+            'meta': {
+                'node': {
+                    "id": node.id,
+                    "key": node.key,
+                    "value": node.value,
+                },
+                'type': 'node'
+            }
         }
-    }
-    tree_node = TreeNode(**data)
-    return tree_node
+        tree_node = TreeNode(**data)
+        return tree_node
+
+    @staticmethod
+    def parse_asset_to_tree_node(node, asset, system_users):
+        icon_skin = 'file'
+        if asset.platform.lower() == 'windows':
+            icon_skin = 'windows'
+        elif asset.platform.lower() == 'linux':
+            icon_skin = 'linux'
+        _system_users = []
+        for system_user in system_users:
+            _system_users.append({
+                'id': system_user.id,
+                'name': system_user.name,
+                'username': system_user.username,
+                'protocol': system_user.protocol,
+                'priority': system_user.priority,
+                'login_mode': system_user.login_mode,
+                'actions': [Action.value_to_choices(system_user.actions)],
+            })
+        data = {
+            'id': str(asset.id),
+            'name': asset.hostname,
+            'title': asset.ip,
+            'pId': node.key,
+            'isParent': False,
+            'open': False,
+            'iconSkin': icon_skin,
+            'meta': {
+                'system_users': _system_users,
+                'type': 'asset',
+                'asset': {
+                    'id': asset.id,
+                    'hostname': asset.hostname,
+                    'ip': asset.ip,
+                    'protocols': asset.protocols_as_list,
+                    'platform': asset.platform,
+                },
+            }
+        }
+        tree_node = TreeNode(**data)
+        return tree_node
diff --git a/apps/perms/utils/stack.py b/apps/perms/utils/stack.py
new file mode 100644
index 000000000..ac2e8b69f
--- /dev/null
+++ b/apps/perms/utils/stack.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+#
+from collections import defaultdict
+from common.struct import Stack
+from common.utils import timeit
+from assets.utils import NodeUtil
+
+
+class PermStackUtilMixin:
+    def __init__(self, debug=False):
+        self.stack = None
+        self._nodes = {}
+        self._debug = debug
+
+    @staticmethod
+    def sorted_by(node_dict):
+        return [int(i) for i in node_dict['key'].split(':')]
+
+    @staticmethod
+    def is_children(item1, item2):
+        key1 = item1["key"]
+        key2 = item2["key"]
+        return key2.startswith(key1 + ':') and (
+            len(key2.split(':')) - len(key1.split(':'))
+        ) == 1
+
+    def debug(self, msg):
+        self._debug and print(msg)
+
+
+class PermSystemUserNodeUtil(PermStackUtilMixin):
+    """
+    self._nodes: {node.key: {system_user.id: actions,}}
+    """
+    @timeit
+    def get_nodes_family_and_system_users(self, nodes_with_system_users):
+        """
+        返回所有nodes_with_system_users中的node的家族节点的信息,
+        并子会继承祖先的系统用户和actions信息
+        :param nodes_with_system_users:
+        {node.key: {system_user.id: actions,}, }
+        :return:
+        {node.key: {system_user.id: actions,}, }
+        """
+        node_util = NodeUtil()
+        _nodes_keys = nodes_with_system_users.keys()
+        family_keys = node_util.get_some_nodes_family_keys_by_keys(_nodes_keys)
+
+        nodes_items = []
+        for i in family_keys:
+            system_users = nodes_with_system_users.get(i, defaultdict(int))
+            item = {"key": i, "system_users": system_users}
+            nodes_items.append(item)
+        # 按照父子关系排序
+        nodes_items.sort(key=self.sorted_by)
+        nodes_items.append({"key": "", "system_users": defaultdict(int)})
+
+        self.stack = Stack()
+        for item in nodes_items:
+            self.debug("准备: {} 栈顶: {}".format(
+                item['key'], self.stack.top["key"] if self.stack.top else None)
+            )
+            # 入栈之前检查,该节点是不是栈顶节点的子节点
+            # 如果不是,则栈顶出栈
+            while self.stack.top and not self.is_children(self.stack.top, item):
+                # 出栈
+                self.pop_from_stack_system_users()
+            # 入栈
+            self.push_to_stack_system_users(item)
+        # 出栈最后一个
+        self.debug("剩余: {}".format(', '.join([n["key"] for n in self.stack])))
+        return self._nodes
+
+    def push_to_stack_system_users(self, item):
+        """
+        :param item:
+        {"key": node.key, "system_users": {system_user.id: actions,},}
+        """
+        if not self.stack.is_empty():
+            item_system_users = item["system_users"]
+            for system_user, action in self.stack.top["system_users"].items():
+                # 更新栈顶的系统用户和action到将要入栈的item中
+                item_system_users[system_user] |= action
+            item["system_users"] = item_system_users
+        self.debug("入栈: {}".format(item['key']))
+        self.stack.push(item)
+
+    # 出栈
+    def pop_from_stack_system_users(self):
+        _node = self.stack.pop()
+        self._nodes[_node["key"]] = _node["system_users"]
+        self.debug("出栈: {} 栈顶: {}".format(_node['key'], self.stack.top['key'] if self.stack.top else None))
+
+
+class PermAssetsAmountUtil(PermStackUtilMixin):
+    def push_to_stack_nodes_amount(self, item):
+        self.debug("入栈: {}".format(item['key']))
+        self.stack.push(item)
+
+    def pop_from_stack_nodes_amount(self):
+        _node = self.stack.pop()
+        self.debug("出栈: {} 栈顶: {}".format(
+            _node['key'], self.stack.top['key'] if self.stack.top else None)
+        )
+        _node["assets_amount"] = len(_node["all_assets"] | _node["assets"])
+        self._nodes[_node.pop("key")] = _node
+
+        if not self.stack.top:
+            return
+        self.stack.top["all_assets"]\
+            .update(_node["all_assets"] | _node["assets"])
+
+    def compute_nodes_assets_amount(self, nodes_with_assets):
+        self.stack = Stack()
+        nodes_items = []
+        for key, values in nodes_with_assets.items():
+            nodes_items.append({
+                "key": key, "assets": values["assets"],
+                "all_assets": values["all_assets"], "assets_amount": 0
+            })
+
+        nodes_items.sort(key=self.sorted_by)
+        nodes_items.append({"key": "", "assets": set(), "all_assets": set(), "assets_amount": 0})
+        self.stack = Stack()
+        for item in nodes_items:
+            self.debug("准备: {} 栈顶: {}".format(
+                item['key'], self.stack.top["key"] if self.stack.top else None)
+            )
+            # 入栈之前检查,该节点是不是栈顶节点的子节点
+            # 如果不是,则栈顶出栈
+            while self.stack.top and not self.is_children(self.stack.top, item):
+                self.pop_from_stack_nodes_amount()
+            self.push_to_stack_nodes_amount(item)
+        # 出栈最后一个
+        self.debug("剩余: {}".format(', '.join([n["key"] for n in self.stack])))
+        return self._nodes
\ No newline at end of file
diff --git a/apps/users/models/user.py b/apps/users/models/user.py
index dbc336443..7b8395859 100644
--- a/apps/users/models/user.py
+++ b/apps/users/models/user.py
@@ -27,97 +27,7 @@ signer = get_signer()
 logger = get_logger(__file__)
 
 
-class User(AbstractUser):
-    ROLE_ADMIN = 'Admin'
-    ROLE_USER = 'User'
-    ROLE_APP = 'App'
-    ROLE_AUDITOR = 'Auditor'
-
-    ROLE_CHOICES = (
-        (ROLE_ADMIN, _('Administrator')),
-        (ROLE_USER, _('User')),
-        (ROLE_APP, _('Application')),
-        (ROLE_AUDITOR, _("Auditor"))
-    )
-    OTP_LEVEL_CHOICES = (
-        (0, _('Disable')),
-        (1, _('Enable')),
-        (2, _("Force enable")),
-    )
-    SOURCE_LOCAL = 'local'
-    SOURCE_LDAP = 'ldap'
-    SOURCE_OPENID = 'openid'
-    SOURCE_RADIUS = 'radius'
-    SOURCE_CHOICES = (
-        (SOURCE_LOCAL, 'Local'),
-        (SOURCE_LDAP, 'LDAP/AD'),
-        (SOURCE_OPENID, 'OpenID'),
-        (SOURCE_RADIUS, 'Radius'),
-    )
-
-    CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
-
-    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
-    username = models.CharField(
-        max_length=128, unique=True, verbose_name=_('Username')
-    )
-    name = models.CharField(max_length=128, verbose_name=_('Name'))
-    email = models.EmailField(
-        max_length=128, unique=True, verbose_name=_('Email')
-    )
-    groups = models.ManyToManyField(
-        'users.UserGroup', related_name='users',
-        blank=True, verbose_name=_('User group')
-    )
-    role = models.CharField(
-        choices=ROLE_CHOICES, default='User', max_length=10,
-        blank=True, verbose_name=_('Role')
-    )
-    avatar = models.ImageField(
-        upload_to="avatar", null=True, verbose_name=_('Avatar')
-    )
-    wechat = models.CharField(
-        max_length=128, blank=True, verbose_name=_('Wechat')
-    )
-    phone = models.CharField(
-        max_length=20, blank=True, null=True, verbose_name=_('Phone')
-    )
-    otp_level = models.SmallIntegerField(
-        default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('MFA')
-    )
-    otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True)
-    # Todo: Auto generate key, let user download
-    private_key = fields.EncryptTextField(
-        blank=True, null=True, verbose_name=_('Private key')
-    )
-    public_key = fields.EncryptTextField(
-        blank=True, null=True, verbose_name=_('Public key')
-    )
-    comment = models.TextField(
-        blank=True, null=True, verbose_name=_('Comment')
-    )
-    is_first_login = models.BooleanField(default=True)
-    date_expired = models.DateTimeField(
-        default=date_expired_default, blank=True, null=True,
-        db_index=True, verbose_name=_('Date expired')
-    )
-    created_by = models.CharField(
-        max_length=30, default='', verbose_name=_('Created by')
-    )
-    source = models.CharField(
-        max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES,
-        verbose_name=_('Source')
-    )
-    date_password_last_updated = models.DateTimeField(
-        auto_now_add=True, blank=True, null=True,
-        verbose_name=_('Date password last updated')
-    )
-
-    user_cache_key_prefix = '_User_{}'
-
-    def __str__(self):
-        return '{0.name}({0.username})'.format(self)
-
+class AuthMixin:
     @property
     def password_raw(self):
         raise AttributeError('Password raw is not a readable attribute')
@@ -134,9 +44,12 @@ class User(AbstractUser):
     def set_password(self, raw_password):
         self._set_password = True
         if self.can_update_password():
-            return super().set_password(raw_password)
+            self.date_password_last_updated = timezone.now()
+            super().set_password(raw_password)
+            self.save()
         else:
-            error = _("User auth from {}, go there change password").format(self.source)
+            error = _("User auth from {}, go there change password").format(
+                self.source)
             raise PermissionError(error)
 
     def can_update_password(self):
@@ -146,9 +59,6 @@ class User(AbstractUser):
         from ..utils import check_otp_code
         return check_otp_code(self.otp_secret_key, code)
 
-    def get_absolute_url(self):
-        return reverse('users:user-detail', args=(self.id,))
-
     def is_public_key_valid(self):
         """
             Check if the user's ssh public key is valid.
@@ -158,36 +68,12 @@ class User(AbstractUser):
             return True
         return False
 
-    @property
-    def groups_display(self):
-        return ' '.join([group.name for group in self.groups.all()])
-
-    @property
-    def role_display(self):
-        return self.get_role_display()
-
-    @property
-    def source_display(self):
-        return self.get_source_display()
-
-    @property
-    def is_expired(self):
-        if self.date_expired and self.date_expired < timezone.now():
-            return True
-        else:
-            return False
-
-    @property
-    def is_valid(self):
-        if self.is_active and not self.is_expired:
-            return True
-        return False
-
     @property
     def public_key_obj(self):
         class PubKey(object):
             def __getattr__(self, item):
                 return ''
+
         if self.public_key:
             import sshpubkeys
             try:
@@ -196,6 +82,52 @@ class User(AbstractUser):
                 pass
         return PubKey()
 
+    def reset_password(self, new_password):
+        self.set_password(new_password)
+
+    @property
+    def date_password_expired(self):
+        interval = settings.SECURITY_PASSWORD_EXPIRATION_TIME
+        date_expired = self.date_password_last_updated + timezone.timedelta(
+            days=int(interval))
+        return date_expired
+
+    @property
+    def password_expired_remain_days(self):
+        date_remain = self.date_password_expired - timezone.now()
+        return date_remain.days
+
+    @property
+    def password_has_expired(self):
+        if self.is_local and self.password_expired_remain_days < 0:
+            return True
+        return False
+
+    @property
+    def password_will_expired(self):
+        if self.is_local and self.password_expired_remain_days < 5:
+            return True
+        return False
+
+
+class RoleMixin:
+    ROLE_ADMIN = 'Admin'
+    ROLE_USER = 'User'
+    ROLE_APP = 'App'
+    ROLE_AUDITOR = 'Auditor'
+
+    ROLE_CHOICES = (
+        (ROLE_ADMIN, _('Administrator')),
+        (ROLE_USER, _('User')),
+        (ROLE_APP, _('Application')),
+        (ROLE_AUDITOR, _("Auditor"))
+    )
+    role = ROLE_USER
+
+    @property
+    def role_display(self):
+        return self.get_role_display()
+
     @property
     def is_superuser(self):
         if self.role == 'Admin':
@@ -251,41 +183,21 @@ class User(AbstractUser):
     def is_staff(self, value):
         pass
 
-    @property
-    def is_local(self):
-        return self.source == self.SOURCE_LOCAL
-    
-    @property
-    def date_password_expired(self):
-        interval = settings.SECURITY_PASSWORD_EXPIRATION_TIME
-        date_expired = self.date_password_last_updated + timezone.timedelta(
-            days=int(interval))
-        return date_expired
+    @classmethod
+    def create_app_user(cls, name, comment):
+        app = cls.objects.create(
+            username=name, name=name, email='{}@local.domain'.format(name),
+            is_active=False, role='App', comment=comment,
+            is_first_login=False, created_by='System'
+        )
+        access_key = app.create_access_key()
+        return app, access_key
 
-    @property
-    def password_expired_remain_days(self):
-        date_remain = self.date_password_expired - timezone.now()
-        return date_remain.days
 
-    @property
-    def password_has_expired(self):
-        if self.is_local and self.password_expired_remain_days < 0:
-            return True
-        return False
-
-    @property
-    def password_will_expired(self):
-        if self.is_local and self.password_expired_remain_days < 5:
-            return True
-        return False
-
-    def save(self, *args, **kwargs):
-        if not self.name:
-            self.name = self.username
-        if self.username == 'admin':
-            self.role = 'Admin'
-            self.is_active = True
-        super().save(*args, **kwargs)
+class TokenMixin:
+    CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
+    email = ''
+    id = None
 
     @property
     def private_token(self):
@@ -333,31 +245,12 @@ class User(AbstractUser):
     def access_key(self):
         return self.access_keys.first()
 
-    def is_member_of(self, user_group):
-        if user_group in self.groups.all():
-            return True
-        return False
-
-    def avatar_url(self):
-        admin_default = settings.STATIC_URL + "img/avatar/admin.png"
-        user_default = settings.STATIC_URL + "img/avatar/user.png"
-        if self.avatar:
-            return self.avatar.url
-        if self.is_superuser:
-            return admin_default
-        else:
-            return user_default
-
     def generate_reset_token(self):
         letter = string.ascii_letters + string.digits
         token = ''.join([random.choice(letter) for _ in range(50)])
         self.set_cache(token)
         return token
 
-    def set_cache(self, token):
-        key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
-        cache.set(key, {'id': self.id, 'email': self.email}, 3600)
-
     @classmethod
     def validate_reset_password_token(cls, token):
         try:
@@ -371,11 +264,25 @@ class User(AbstractUser):
             user = None
         return user
 
+    def set_cache(self, token):
+        key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
+        cache.set(key, {'id': self.id, 'email': self.email}, 3600)
+
     @classmethod
     def expired_reset_password_token(cls, token):
         key = cls.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
         cache.delete(key)
 
+
+class MFAMixin:
+    otp_level = 0
+    otp_secret_key = ''
+    OTP_LEVEL_CHOICES = (
+        (0, _('Disable')),
+        (1, _('Enable')),
+        (2, _("Force enable")),
+    )
+
     @property
     def otp_enabled(self):
         return self.otp_force_enabled or self.otp_level > 0
@@ -397,39 +304,130 @@ class User(AbstractUser):
         self.otp_level = 0
         self.otp_secret_key = None
 
-    def to_json(self):
-        return OrderedDict({
-            'id': self.id,
-            'username': self.username,
-            'name': self.name,
-            'email': self.email,
-            'is_active': self.is_active,
-            'is_superuser': self.is_superuser,
-            'role': self.get_role_display(),
-            'groups': [group.name for group in self.groups.all()],
-            'source': self.get_source_display(),
-            'wechat': self.wechat,
-            'phone': self.phone,
-            'otp_level': self.otp_level,
-            'comment': self.comment,
-            'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') \
-                if self.date_expired is not None else None
-        })
 
-    @classmethod
-    def create_app_user(cls, name, comment):
-        app = cls.objects.create(
-            username=name, name=name, email='{}@local.domain'.format(name),
-            is_active=False, role='App', comment=comment,
-            is_first_login=False, created_by='System'
-        )
-        access_key = app.create_access_key()
-        return app, access_key
+class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
+    SOURCE_LOCAL = 'local'
+    SOURCE_LDAP = 'ldap'
+    SOURCE_OPENID = 'openid'
+    SOURCE_RADIUS = 'radius'
+    SOURCE_CHOICES = (
+        (SOURCE_LOCAL, 'Local'),
+        (SOURCE_LDAP, 'LDAP/AD'),
+        (SOURCE_OPENID, 'OpenID'),
+        (SOURCE_RADIUS, 'Radius'),
+    )
 
-    def reset_password(self, new_password):
-        self.set_password(new_password)
-        self.date_password_last_updated = timezone.now()
-        self.save()
+    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
+    username = models.CharField(
+        max_length=128, unique=True, verbose_name=_('Username')
+    )
+    name = models.CharField(max_length=128, verbose_name=_('Name'))
+    email = models.EmailField(
+        max_length=128, unique=True, verbose_name=_('Email')
+    )
+    groups = models.ManyToManyField(
+        'users.UserGroup', related_name='users',
+        blank=True, verbose_name=_('User group')
+    )
+    role = models.CharField(
+        choices=RoleMixin.ROLE_CHOICES, default='User', max_length=10,
+        blank=True, verbose_name=_('Role')
+    )
+    avatar = models.ImageField(
+        upload_to="avatar", null=True, verbose_name=_('Avatar')
+    )
+    wechat = models.CharField(
+        max_length=128, blank=True, verbose_name=_('Wechat')
+    )
+    phone = models.CharField(
+        max_length=20, blank=True, null=True, verbose_name=_('Phone')
+    )
+    otp_level = models.SmallIntegerField(
+        default=0, choices=MFAMixin.OTP_LEVEL_CHOICES, verbose_name=_('MFA')
+    )
+    otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True)
+    # Todo: Auto generate key, let user download
+    private_key = fields.EncryptTextField(
+        blank=True, null=True, verbose_name=_('Private key')
+    )
+    public_key = fields.EncryptTextField(
+        blank=True, null=True, verbose_name=_('Public key')
+    )
+    comment = models.TextField(
+        blank=True, null=True, verbose_name=_('Comment')
+    )
+    is_first_login = models.BooleanField(default=True)
+    date_expired = models.DateTimeField(
+        default=date_expired_default, blank=True, null=True,
+        db_index=True, verbose_name=_('Date expired')
+    )
+    created_by = models.CharField(
+        max_length=30, default='', verbose_name=_('Created by')
+    )
+    source = models.CharField(
+        max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES,
+        verbose_name=_('Source')
+    )
+    date_password_last_updated = models.DateTimeField(
+        auto_now_add=True, blank=True, null=True,
+        verbose_name=_('Date password last updated')
+    )
+
+    user_cache_key_prefix = '_User_{}'
+
+    def __str__(self):
+        return '{0.name}({0.username})'.format(self)
+
+    def get_absolute_url(self):
+        return reverse('users:user-detail', args=(self.id,))
+
+    @property
+    def groups_display(self):
+        return ' '.join([group.name for group in self.groups.all()])
+
+    @property
+    def source_display(self):
+        return self.get_source_display()
+
+    @property
+    def is_expired(self):
+        if self.date_expired and self.date_expired < timezone.now():
+            return True
+        else:
+            return False
+
+    @property
+    def is_valid(self):
+        if self.is_active and not self.is_expired:
+            return True
+        return False
+
+    @property
+    def is_local(self):
+        return self.source == self.SOURCE_LOCAL
+    
+    def save(self, *args, **kwargs):
+        if not self.name:
+            self.name = self.username
+        if self.username == 'admin':
+            self.role = 'Admin'
+            self.is_active = True
+        super().save(*args, **kwargs)
+
+    def is_member_of(self, user_group):
+        if user_group in self.groups.all():
+            return True
+        return False
+
+    def avatar_url(self):
+        admin_default = settings.STATIC_URL + "img/avatar/admin.png"
+        user_default = settings.STATIC_URL + "img/avatar/user.png"
+        if self.avatar:
+            return self.avatar.url
+        if self.is_superuser:
+            return admin_default
+        else:
+            return user_default
 
     def delete(self, using=None, keep_parents=False):
         if self.pk == 1 or self.username == 'admin':
diff --git a/apps/users/templates/users/user_granted_asset.html b/apps/users/templates/users/user_granted_asset.html
index 41aea998c..225005ffd 100644
--- a/apps/users/templates/users/user_granted_asset.html
+++ b/apps/users/templates/users/user_granted_asset.html
@@ -29,7 +29,6 @@
                                    <div class="file-manager ">
                                        <div id="assetTree" class="ztree">
                                        </div>
-
                                        <div class="clearfix"></div>
                                    </div>
                                </div>
@@ -43,7 +42,6 @@
                                             <th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
                                             <th class="text-center">{% trans 'Hostname' %}</th>
                                             <th class="text-center">{% trans 'IP' %}</th>
-                                            <th class="text-center">{% trans 'Active' %}</th>
                                             <th class="text-center">{% trans 'System users' %}</th>
                                         </tr>
                                     </thead>
@@ -64,7 +62,7 @@ var zTree;
 var inited = false;
 var url;
 var asset_table;
-var treeUrl = "{% url 'api-perms:user-nodes-assets-as-tree' pk=object.id %}?show_assets=0&cache_policy=1";
+var treeUrl = "{% url 'api-perms:user-nodes-as-tree' pk=object.id %}?&cache_policy=1";
 
 function initTable() {
     if (inited){
@@ -83,13 +81,6 @@ function initTable() {
                 $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
             }},
             {targets: 3, createdCell: function (td, cellData) {
-                if (!cellData) {
-                    $(td).html('<i class="fa fa-times text-danger"></i>')
-                } else {
-                    $(td).html('<i class="fa fa-check text-navy"></i>')
-                }
-            }},
-            {targets: 4, createdCell: function (td, cellData) {
                 var users = [];
                 $.each(cellData, function (id, data) {
                     var name = htmlEscape(data.name);
@@ -101,7 +92,6 @@ function initTable() {
         ajax_url: url,
         columns: [
             {data: "id"}, {data: "hostname" }, {data: "ip" },
-            {data: "is_active", orderable: false },
             {data: "system_users_granted", orderable: false}
         ]
     };