From 20f3e917fecfba464a8dbb4ad8732d300004b339 Mon Sep 17 00:00:00 2001
From: pycook <pycook@126.com>
Date: Fri, 22 Dec 2023 14:32:03 +0800
Subject: [PATCH]  pref(api): import and export of CIType templates

 pref(api): import and export of CIType templates
---
 cmdb-api/api/lib/cmdb/attribute.py   |   3 +-
 cmdb-api/api/lib/cmdb/ci_type.py     | 437 +++++++++++++++++++--------
 cmdb-api/api/lib/cmdb/const.py       |   1 +
 cmdb-api/api/lib/cmdb/perms.py       |   5 +-
 cmdb-api/api/lib/cmdb/resp_format.py |   2 +
 cmdb-api/api/lib/perm/acl/acl.py     |  14 +-
 cmdb-api/api/lib/perm/acl/audit.py   |   6 +
 cmdb-api/api/lib/perm/acl/user.py    |   1 +
 cmdb-api/api/models/acl.py           |   2 +-
 cmdb-api/api/tasks/acl.py            |  15 +-
 cmdb-api/api/views/cmdb/ci_type.py   |  36 ++-
 11 files changed, 363 insertions(+), 159 deletions(-)

diff --git a/cmdb-api/api/lib/cmdb/attribute.py b/cmdb-api/api/lib/cmdb/attribute.py
index 817bac2..37aa31a 100644
--- a/cmdb-api/api/lib/cmdb/attribute.py
+++ b/cmdb-api/api/lib/cmdb/attribute.py
@@ -189,7 +189,8 @@ class AttributeManager(object):
         return attr
 
     def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True):
-        attr = AttributeCache.get(key).to_dict()
+        attr = AttributeCache.get(key) or dict()
+        attr = attr and attr.to_dict()
         if attr.get("is_choice"):
             attr["choice_value"] = self.get_choice_values(
                 attr["id"],
diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py
index b7e74d5..79c963d 100644
--- a/cmdb-api/api/lib/cmdb/ci_type.py
+++ b/cmdb-api/api/lib/cmdb/ci_type.py
@@ -5,8 +5,10 @@ import copy
 import toposort
 from flask import abort
 from flask import current_app
+from flask import session
 from flask_login import current_user
 from toposort import toposort_flatten
+from werkzeug.exceptions import BadRequest
 
 from api.extensions import db
 from api.lib.cmdb.attribute import AttributeManager
@@ -75,12 +77,13 @@ class CITypeManager(object):
     def get_ci_types(type_name=None):
         resources = None
         if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
-            resources = set([i.get('name') for i in ACLManager().get_resources("CIType")])
+            resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)])
 
         ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name)
         res = list()
         for type_dict in ci_types:
-            type_dict["unique_key"] = AttributeCache.get(type_dict["unique_id"]).name
+            attr = AttributeCache.get(type_dict["unique_id"])
+            type_dict["unique_key"] = attr and attr.name
             if resources is None or type_dict['name'] in resources:
                 res.append(type_dict)
 
@@ -113,6 +116,9 @@ class CITypeManager(object):
     @classmethod
     @kwargs_required("name")
     def add(cls, **kwargs):
+        if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
+            if ErrFormat.ci_type_config not in {i['name'] for i in ACLManager().get_resources(ResourceTypeEnum.PAGE)}:
+                return abort(403, ErrFormat.no_permission2)
 
         unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None)
         unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
@@ -131,7 +137,11 @@ class CITypeManager(object):
         CITypeCache.clean(ci_type.name)
 
         if current_app.config.get("USE_ACL"):
-            ACLManager().add_resource(ci_type.name, ResourceTypeEnum.CI)
+            try:
+                ACLManager().add_resource(ci_type.name, ResourceTypeEnum.CI)
+            except BadRequest:
+                pass
+
             ACLManager().grant_resource_to_role(ci_type.name,
                                                 RoleEnum.CMDB_READ_ALL,
                                                 ResourceTypeEnum.CI,
@@ -243,7 +253,6 @@ class CITypeGroupManager(object):
             else:
                 resources = {i['name']: i['permissions'] for i in resources if PermEnum.READ in i.get("permissions")}
 
-        current_app.logger.info(resources)
         groups = sorted(CITypeGroup.get_by(), key=lambda x: x['order'] or 0)
         group_types = set()
         for group in groups:
@@ -283,7 +292,10 @@ class CITypeGroupManager(object):
         """
         existed = CITypeGroup.get_by_id(gid) or abort(
             404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
-        if name is not None:
+        if name is not None and name != existed.name:
+            if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
+                return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
+
             existed.update(name=name)
 
         max_order = max([i.order or 0 for i in CITypeGroupItem.get_by(group_id=gid, to_dict=False)] or [0])
@@ -725,7 +737,7 @@ class CITypeAttributeGroupManager(object):
         grouped = list()
 
         attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id)
-        id2attr = {i['id']: i for i in attributes}
+        id2attr = {i.get('id'): i for i in attributes}
 
         for group in groups:
             items = CITypeAttributeGroupItem.get_by(group_id=group["id"], to_dict=False)
@@ -891,97 +903,58 @@ class CITypeAttributeGroupManager(object):
 
 class CITypeTemplateManager(object):
     @staticmethod
-    def __import(cls, data):
-        id2obj_dicts = {i['id']: i for i in data}
-        existed = cls.get_by(deleted=None, to_dict=False)
-        id2existed = {i.id: i for i in existed}
-        existed_ids = [i.id for i in existed]
-        existed_no_delete_ids = [i.id for i in existed if not i.deleted]
+    def __import(cls, data, unique_key='name'):
+        id2obj_dicts = {i[unique_key]: i for i in data}
+        existed = cls.get_by(to_dict=False)
+        id2existed = {getattr(i, unique_key): i for i in existed}
+        existed_ids = [getattr(i, unique_key) for i in existed]
 
+        id_map = dict()
         # add
         for added_id in set(id2obj_dicts.keys()) - set(existed_ids):
+            _id = id2obj_dicts[added_id].pop('id', None)
+            id2obj_dicts[added_id].pop('created_at', None)
+            id2obj_dicts[added_id].pop('updated_at', None)
+            id2obj_dicts[added_id].pop('uid', None)
+
             if cls == CIType:
-                CITypeManager.add(**id2obj_dicts[added_id])
+                __id = CITypeManager.add(**id2obj_dicts[added_id])
+                CITypeCache.clean(__id)
             elif cls == CITypeRelation:
-                CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'),
-                                          id2obj_dicts[added_id].get('child_id'),
-                                          id2obj_dicts[added_id].get('relation_type_id'),
-                                          id2obj_dicts[added_id].get('constraint'),
-                                          )
+                __id = CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'),
+                                                 id2obj_dicts[added_id].get('child_id'),
+                                                 id2obj_dicts[added_id].get('relation_type_id'),
+                                                 id2obj_dicts[added_id].get('constraint'),
+                                                 )
             else:
-                cls.create(flush=True, **id2obj_dicts[added_id])
+                obj = cls.create(flush=True, **id2obj_dicts[added_id])
+                if cls == Attribute:
+                    AttributeCache.clean(obj)
+                __id = obj.id
+
+            id_map[_id] = __id
 
         # update
         for updated_id in set(id2obj_dicts.keys()) & set(existed_ids):
+            _id = id2obj_dicts[updated_id].pop('id', None)
+
+            id2existed[updated_id].update(flush=True, **id2obj_dicts[updated_id])
+
+            id_map[_id] = id2existed[updated_id].id
+
+            if cls == Attribute:
+                AttributeCache.clean(id2existed[updated_id])
+
             if cls == CIType:
-                deleted = id2existed[updated_id].deleted
-                CITypeManager.update(updated_id, **id2obj_dicts[updated_id])
-                if deleted and current_app.config.get("USE_ACL"):
-                    type_name = id2obj_dicts[updated_id]['name']
-                    ACLManager().add_resource(type_name, ResourceTypeEnum.CI)
-                    ACLManager().grant_resource_to_role(type_name,
-                                                        RoleEnum.CMDB_READ_ALL,
-                                                        ResourceTypeEnum.CI,
-                                                        permissions=[PermEnum.READ])
-                    ACLManager().grant_resource_to_role(type_name,
-                                                        current_user.username,
-                                                        ResourceTypeEnum.CI)
+                CITypeCache.clean(id2existed[updated_id].id)
 
-            else:
-                id2existed[updated_id].update(flush=True, **id2obj_dicts[updated_id])
-
-        # delete
-        for deleted_id in set(existed_no_delete_ids) - set(id2obj_dicts.keys()):
-            if cls == CIType:
-                id2existed[deleted_id].soft_delete(flush=True)
-
-                CITypeCache.clean(deleted_id)
-
-                CITypeHistoryManager.add(CITypeOperateType.DELETE, deleted_id, change=id2existed[deleted_id].to_dict())
-
-                if current_app.config.get("USE_ACL"):
-                    ACLManager().del_resource(id2existed[deleted_id].name, ResourceTypeEnum.CI)
-            else:
-                id2existed[deleted_id].soft_delete(flush=True)
         try:
             db.session.commit()
         except Exception as e:
             db.session.rollback()
             raise Exception(str(e))
 
-    def _import_ci_types(self, ci_types):
-        for i in ci_types:
-            i.pop("unique_key", None)
-
-        self.__import(CIType, ci_types)
-
-    def _import_ci_type_groups(self, ci_type_groups):
-        _ci_type_groups = copy.deepcopy(ci_type_groups)
-        for i in _ci_type_groups:
-            i.pop('ci_types', None)
-
-        self.__import(CITypeGroup, _ci_type_groups)
-
-        # import group type items
-        for group in ci_type_groups:
-            existed = CITypeGroupItem.get_by(group_id=group['id'], to_dict=False)
-            for i in existed:
-                i.soft_delete()
-
-            for order, ci_type in enumerate(group.get('ci_types') or []):
-                payload = dict(group_id=group['id'], type_id=ci_type['id'], order=order)
-                CITypeGroupItem.create(**payload)
-
-    def _import_relation_types(self, relation_types):
-        self.__import(RelationType, relation_types)
-
-    def _import_ci_type_relations(self, ci_type_relations):
-        for i in ci_type_relations:
-            i.pop('parent', None)
-            i.pop('child', None)
-            i.pop('relation_type', None)
-
-        self.__import(CITypeRelation, ci_type_relations)
+        return id_map
 
     def _import_attributes(self, type2attributes):
         attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
@@ -990,122 +963,262 @@ class CITypeTemplateManager(object):
             i.pop('default_show', None)
             i.pop('is_required', None)
             i.pop('order', None)
+            i.pop('choice_web_hook', None)
+            i.pop('choice_other', None)
+            i.pop('order', None)
             choice_value = i.pop('choice_value', None)
+            if not choice_value:
+                i['is_choice'] = False
 
             attrs.append((i, choice_value))
 
-        self.__import(Attribute, [i[0] for i in attrs])
+        attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)])
 
         for i, choice_value in attrs:
-            if choice_value:
-                AttributeManager.add_choice_values(i['id'], i['value_type'], choice_value)
+            if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'):
+                AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value)
+
+        return attr_id_map
+
+    def _import_ci_types(self, ci_types, attr_id_map):
+        for i in ci_types:
+            i.pop("unique_key", None)
+            i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id'])
+            i['uid'] = current_user.uid
+
+        return self.__import(CIType, ci_types)
+
+    def _import_ci_type_groups(self, ci_type_groups, type_id_map):
+        _ci_type_groups = copy.deepcopy(ci_type_groups)
+        for i in _ci_type_groups:
+            i.pop('ci_types', None)
+
+        group_id_map = self.__import(CITypeGroup, _ci_type_groups)
+
+        # import group type items
+        for group in ci_type_groups:
+            for order, ci_type in enumerate(group.get('ci_types') or []):
+                payload = dict(group_id=group_id_map.get(group['id'], group['id']),
+                               type_id=type_id_map.get(ci_type['id'], ci_type['id']),
+                               order=order)
+                existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
+                                                 first=True, to_dict=False)
+                if existed is None:
+                    CITypeGroupItem.create(flush=True, **payload)
+                else:
+                    existed.update(flush=True, **payload)
+
+        try:
+            db.session.commit()
+        except Exception as e:
+            db.session.rollback()
+            raise Exception(str(e))
+
+    def _import_relation_types(self, relation_types):
+        return self.__import(RelationType, relation_types)
 
     @staticmethod
-    def _import_type_attributes(type2attributes):
-        # add type attribute
+    def _import_ci_type_relations(ci_type_relations, type_id_map, relation_type_id_map):
+        for i in ci_type_relations:
+            i.pop('parent', None)
+            i.pop('child', None)
+            i.pop('relation_type', None)
+
+            i['parent_id'] = type_id_map.get(i['parent_id'], i['parent_id'])
+            i['child_id'] = type_id_map.get(i['child_id'], i['child_id'])
+            i['relation_type_id'] = relation_type_id_map.get(i['relation_type_id'], i['relation_type_id'])
+
+            try:
+                CITypeRelationManager.add(i.get('parent_id'),
+                                          i.get('child_id'),
+                                          i.get('relation_type_id'),
+                                          i.get('constraint'),
+                                          )
+            except BadRequest:
+                pass
+
+    @staticmethod
+    def _import_type_attributes(type2attributes, type_id_map, attr_id_map):
+        for type_id in type2attributes:
+            CITypeAttributesCache.clean(type_id_map.get(int(type_id), type_id))
 
         for type_id in type2attributes:
-            existed = CITypeAttribute.get_by(type_id=type_id, to_dict=False)
-            existed_attr_ids = {i.attr_id: i for i in existed}
-            new_attr_ids = {i['id']: i for i in type2attributes[type_id]}
+            existed = CITypeAttributesCache.get2(type_id_map.get(int(type_id), type_id))
+            existed_attr_names = {attr.name: ta for ta, attr in existed}
 
+            handled = set()
             for attr in type2attributes[type_id]:
-                payload = dict(type_id=type_id,
-                               attr_id=attr['id'],
+                payload = dict(type_id=type_id_map.get(int(type_id), type_id),
+                               attr_id=attr_id_map.get(attr['id'], attr['id']),
                                default_show=attr['default_show'],
                                is_required=attr['is_required'],
                                order=attr['order'])
-                if attr['id'] not in existed_attr_ids:  # new
-                    CITypeAttribute.create(flush=True, **payload)
-                else:  # update
-                    existed_attr_ids[attr['id']].update(**payload)
+                if attr['name'] not in handled:
+                    if attr['name'] not in existed_attr_names:  # new
+                        CITypeAttribute.create(flush=True, **payload)
+                    else:  # update
+                        existed_attr_names[attr['name']].update(flush=True, **payload)
 
-            # delete
-            for i in existed:
-                if i.attr_id not in new_attr_ids:
-                    i.soft_delete()
+                    handled.add(attr['name'])
+
+        try:
+            db.session.commit()
+        except Exception as e:
+            db.session.rollback()
+            raise Exception(str(e))
+
+        for type_id in type2attributes:
+            CITypeAttributesCache.clean(type_id_map.get(int(type_id), type_id))
 
     @staticmethod
-    def _import_attribute_group(type2attribute_group):
+    def _import_attribute_group(type2attribute_group, type_id_map, attr_id_map):
         for type_id in type2attribute_group:
-            existed = CITypeAttributeGroup.get_by(type_id=type_id, to_dict=False)
-            for i in existed:
-                i.soft_delete()
-
             for group in type2attribute_group[type_id] or []:
                 _group = copy.deepcopy(group)
                 _group.pop('attributes', None)
                 _group.pop('id', None)
-                new = CITypeAttributeGroup.create(**_group)
+                existed = CITypeAttributeGroup.get_by(name=_group['name'],
+                                                      type_id=type_id_map.get(_group['type_id'], _group['type_id']),
+                                                      first=True, to_dict=False)
+                if existed is None:
+                    _group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id'])
 
-                existed = CITypeAttributeGroupItem.get_by(group_id=new.id, to_dict=False)
-                for i in existed:
-                    i.soft_delete()
+                    existed = CITypeAttributeGroup.create(flush=True, **_group)
 
                 for order, attr in enumerate(group['attributes'] or []):
-                    CITypeAttributeGroupItem.create(group_id=new.id, attr_id=attr['id'], order=order)
+                    item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id,
+                                                                   attr_id=attr_id_map.get(attr['id'], attr['id']),
+                                                                   first=True, to_dict=False)
+                    if item_existed is None:
+                        CITypeAttributeGroupItem.create(group_id=existed.id,
+                                                        attr_id=attr_id_map.get(attr['id'], attr['id']),
+                                                        order=order)
+                    else:
+                        item_existed.update(flush=True, order=order)
+
+        try:
+            db.session.commit()
+        except Exception as e:
+            db.session.rollback()
+            raise Exception(str(e))
 
     @staticmethod
     def _import_auto_discovery_rules(rules):
         from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
         from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
+
         for rule in rules:
             ci_type = CITypeCache.get(rule.pop('type_name', None))
+            adr = rule.pop('adr', {}) or {}
+
             if ci_type:
                 rule['type_id'] = ci_type.id
             if rule.get('adr_name'):
                 ad_rule = AutoDiscoveryRuleCRUD.get_by_name(rule.pop("adr_name"))
+                adr.pop('created_at', None)
+                adr.pop('updated_at', None)
+                adr.pop('id', None)
+
                 if ad_rule:
                     rule['adr_id'] = ad_rule.id
+                    ad_rule.update(**adr)
+
+                elif adr:
+                    ad_rule = AutoDiscoveryRuleCRUD().add(**adr)
+                    rule['adr_id'] = ad_rule.id
+                else:
+                    continue
 
             rule.pop("id", None)
             rule.pop("created_at", None)
             rule.pop("updated_at", None)
 
             rule['uid'] = current_user.uid
-            try:
-                AutoDiscoveryCITypeCRUD.add(**rule)
-            except Exception as e:
-                current_app.logger.warning("import auto discovery rules failed: {}".format(e))
+
+            existed = False
+            for i in AutoDiscoveryCIType.get_by(type_id=ci_type.id, adr_id=rule['adr_id'], to_dict=False):
+                if ((i.extra_option or {}).get('alias') or None) == (
+                        (rule.get('extra_option') or {}).get('alias') or None):
+                    existed = True
+                    AutoDiscoveryCITypeCRUD().update(i.id, **rule)
+                    break
+
+            if not existed:
+                try:
+                    AutoDiscoveryCITypeCRUD().add(**rule)
+                except Exception as e:
+                    current_app.logger.warning("import auto discovery rules failed: {}".format(e))
+
+    @staticmethod
+    def _import_icons(icons):
+        from api.lib.common_setting.upload_file import CommonFileCRUD
+        for icon_name in icons:
+            if icons[icon_name]:
+                try:
+                    CommonFileCRUD().save_str_to_file(icon_name, icons[icon_name])
+                except Exception as e:
+                    current_app.logger.warning("save icon failed: {}".format(e))
 
     def import_template(self, tpt):
         import time
         s = time.time()
-        self._import_attributes(tpt.get('type2attributes') or {})
+        attr_id_map = self._import_attributes(tpt.get('type2attributes') or {})
         current_app.logger.info('import attributes cost: {}'.format(time.time() - s))
 
         s = time.time()
-        self._import_ci_types(tpt.get('ci_types') or [])
+        ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map)
         current_app.logger.info('import ci_types cost: {}'.format(time.time() - s))
 
         s = time.time()
-        self._import_ci_type_groups(tpt.get('ci_type_groups') or [])
+        self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map)
         current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s))
 
         s = time.time()
-        self._import_relation_types(tpt.get('relation_types') or [])
+        relation_type_id_map = self._import_relation_types(tpt.get('relation_types') or [])
         current_app.logger.info('import relation_types cost: {}'.format(time.time() - s))
 
         s = time.time()
-        self._import_ci_type_relations(tpt.get('ci_type_relations') or [])
+        self._import_ci_type_relations(tpt.get('ci_type_relations') or [], ci_type_id_map, relation_type_id_map)
         current_app.logger.info('import ci_type_relations cost: {}'.format(time.time() - s))
 
         s = time.time()
-        self._import_type_attributes(tpt.get('type2attributes') or {})
+        self._import_type_attributes(tpt.get('type2attributes') or {}, ci_type_id_map, attr_id_map)
         current_app.logger.info('import type2attributes cost: {}'.format(time.time() - s))
 
         s = time.time()
-        self._import_attribute_group(tpt.get('type2attribute_group') or {})
+        self._import_attribute_group(tpt.get('type2attribute_group') or {}, ci_type_id_map, attr_id_map)
         current_app.logger.info('import type2attribute_group cost: {}'.format(time.time() - s))
 
         s = time.time()
         self._import_auto_discovery_rules(tpt.get('ci_type_auto_discovery_rules') or [])
         current_app.logger.info('import ci_type_auto_discovery_rules cost: {}'.format(time.time() - s))
 
+        s = time.time()
+        self._import_icons(tpt.get('icons') or {})
+        current_app.logger.info('import icons cost: {}'.format(time.time() - s))
+
     @staticmethod
     def export_template():
         from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
         from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
+        from api.lib.common_setting.upload_file import CommonFileCRUD
+
+        tpt = dict(
+            ci_types=CITypeManager.get_ci_types(),
+            ci_type_groups=CITypeGroupManager.get(),
+            relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
+            ci_type_relations=CITypeRelationManager.get(),
+            ci_type_auto_discovery_rules=list(),
+            type2attributes=dict(),
+            type2attribute_group=dict(),
+            icons=dict()
+        )
+
+        def get_icon_value(icon):
+            try:
+                return CommonFileCRUD().get_file_binary_str(icon)
+            except:
+                return ""
 
         ad_rules = AutoDiscoveryCITypeCRUD.get_all()
         rules = []
@@ -1116,23 +1229,91 @@ class CITypeTemplateManager(object):
             if r.get('adr_id'):
                 adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
                 r['adr_name'] = adr and adr.name
+                r['adr'] = adr and adr.to_dict() or {}
+
+                icon_url = r['adr'].get('option', {}).get('icon', {}).get('url')
+                if icon_url and icon_url not in tpt['icons']:
+                    tpt['icons'][icon_url] = get_icon_value(icon_url)
 
             rules.append(r)
 
-        tpt = dict(
-            ci_types=CITypeManager.get_ci_types(),
-            ci_type_groups=CITypeGroupManager.get(),
-            relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
-            ci_type_relations=CITypeRelationManager.get(),
-            ci_type_auto_discovery_rules=rules,
-            type2attributes=dict(),
-            type2attribute_group=dict()
-        )
+        tpt['ci_type_auto_discovery_rules'] = rules
 
         for ci_type in tpt['ci_types']:
+            if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3:
+                icon_url = ci_type['icon'].split('$$')[3]
+                if icon_url not in tpt['icons']:
+                    tpt['icons'][icon_url] = get_icon_value(icon_url)
+
             tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
                 ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
 
+            for attr in tpt['type2attributes'][ci_type['id']]:
+                for i in (attr.get('choice_value') or []):
+                    if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3:
+                        icon_url = i[1]['icon']['url'].split('$$')[3]
+                        if icon_url not in tpt['icons']:
+                            tpt['icons'][icon_url] = get_icon_value(icon_url)
+
+            tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
+
+        return tpt
+
+    @staticmethod
+    def export_template_by_type(type_id):
+        ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found2.format("id={}".format(type_id)))
+
+        from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
+        from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
+        from api.lib.common_setting.upload_file import CommonFileCRUD
+
+        tpt = dict(
+            ci_types=CITypeManager.get_ci_types(type_name=ci_type.name),
+            ci_type_auto_discovery_rules=list(),
+            type2attributes=dict(),
+            type2attribute_group=dict(),
+            icons=dict()
+        )
+
+        def get_icon_value(icon):
+            try:
+                return CommonFileCRUD().get_file_binary_str(icon)
+            except:
+                return ""
+
+        ad_rules = AutoDiscoveryCITypeCRUD.get_by_type_id(ci_type.id)
+        rules = []
+        for r in ad_rules:
+            r = r.to_dict()
+            r['type_name'] = ci_type and ci_type.name
+            if r.get('adr_id'):
+                adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
+                r['adr_name'] = adr and adr.name
+                r['adr'] = adr and adr.to_dict() or {}
+
+                icon_url = r['adr'].get('option', {}).get('icon', {}).get('url')
+                if icon_url and icon_url not in tpt['icons']:
+                    tpt['icons'][icon_url] = get_icon_value(icon_url)
+
+            rules.append(r)
+        tpt['ci_type_auto_discovery_rules'] = rules
+
+        for ci_type in tpt['ci_types']:
+            if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3:
+                icon_url = ci_type['icon'].split('$$')[3]
+                if icon_url not in tpt['icons']:
+                    tpt['icons'][icon_url] = get_icon_value(icon_url)
+
+            tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
+                ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
+
+            for attr in tpt['type2attributes'][ci_type['id']]:
+                for i in (attr.get('choice_value') or []):
+                    if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3:
+                        icon_url = i[1]['icon']['url'].split('$$')[3]
+                        if icon_url not in tpt['icons']:
+                            tpt['icons'][icon_url] = get_icon_value(icon_url)
+
             tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
 
         return tpt
diff --git a/cmdb-api/api/lib/cmdb/const.py b/cmdb-api/api/lib/cmdb/const.py
index c48fd1b..31cc6e5 100644
--- a/cmdb-api/api/lib/cmdb/const.py
+++ b/cmdb-api/api/lib/cmdb/const.py
@@ -69,6 +69,7 @@ class ResourceTypeEnum(BaseEnum):
     CI_TYPE_RELATION = "CITypeRelation"  # create/delete/grant
     RELATION_VIEW = "RelationView"  # read/update/delete/grant
     CI_FILTER = "CIFilter"  # read
+    PAGE = "page"  # read
 
 
 class PermEnum(BaseEnum):
diff --git a/cmdb-api/api/lib/cmdb/perms.py b/cmdb-api/api/lib/cmdb/perms.py
index 3b5ad6f..f034f37 100644
--- a/cmdb-api/api/lib/cmdb/perms.py
+++ b/cmdb-api/api/lib/cmdb/perms.py
@@ -143,11 +143,14 @@ class CIFilterPermsCRUD(DBMixin):
                               first=True, to_dict=False)
 
         if obj is not None:
+            resource = None
             if current_app.config.get('USE_ACL'):
-                ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
+                resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
 
             obj.soft_delete()
 
+            return resource
+
 
 def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None):
     def decorator_has_perm(func):
diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py
index b7a3495..12a532f 100644
--- a/cmdb-api/api/lib/cmdb/resp_format.py
+++ b/cmdb-api/api/lib/cmdb/resp_format.py
@@ -4,6 +4,8 @@ from api.lib.resp_format import CommonErrFormat
 
 
 class ErrFormat(CommonErrFormat):
+    ci_type_config = "模型配置"
+
     invalid_relation_type = "无效的关系类型: {}"
     ci_type_not_found = "模型不存在!"
     argument_attributes_must_be_list = "参数 attributes 类型必须是列表"
diff --git a/cmdb-api/api/lib/perm/acl/acl.py b/cmdb-api/api/lib/perm/acl/acl.py
index 423d3be..fbb2642 100644
--- a/cmdb-api/api/lib/perm/acl/acl.py
+++ b/cmdb-api/api/lib/perm/acl/acl.py
@@ -117,15 +117,15 @@ class ACLManager(object):
             if group:
                 PermissionCRUD.grant(role.id, permissions, group_id=group.id)
 
-    def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
+    def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True):
         resource = self._get_resource(name, resource_type_name)
 
         if resource:
-            PermissionCRUD.grant(rid, permissions, resource_id=resource.id)
+            PermissionCRUD.grant(rid, permissions, resource_id=resource.id, rebuild=rebuild)
         else:
             group = self._get_resource_group(name)
             if group:
-                PermissionCRUD.grant(rid, permissions, group_id=group.id)
+                PermissionCRUD.grant(rid, permissions, group_id=group.id, rebuild=rebuild)
 
     def revoke_resource_from_role(self, name, role, resource_type_name=None, permissions=None):
         resource = self._get_resource(name, resource_type_name)
@@ -138,21 +138,23 @@ class ACLManager(object):
             if group:
                 PermissionCRUD.revoke(role.id, permissions, group_id=group.id)
 
-    def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
+    def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True):
         resource = self._get_resource(name, resource_type_name)
 
         if resource:
-            PermissionCRUD.revoke(rid, permissions, resource_id=resource.id)
+            PermissionCRUD.revoke(rid, permissions, resource_id=resource.id, rebuild=rebuild)
         else:
             group = self._get_resource_group(name)
             if group:
-                PermissionCRUD.revoke(rid, permissions, group_id=group.id)
+                PermissionCRUD.revoke(rid, permissions, group_id=group.id, rebuild=rebuild)
 
     def del_resource(self, name, resource_type_name=None):
         resource = self._get_resource(name, resource_type_name)
         if resource:
             ResourceCRUD.delete(resource.id)
 
+        return resource
+
     def has_permission(self, resource_name, resource_type, perm, resource_id=None):
         if is_app_admin(self.app_id):
             return True
diff --git a/cmdb-api/api/lib/perm/acl/audit.py b/cmdb-api/api/lib/perm/acl/audit.py
index bfce104..f741345 100644
--- a/cmdb-api/api/lib/perm/acl/audit.py
+++ b/cmdb-api/api/lib/perm/acl/audit.py
@@ -394,4 +394,10 @@ class AuditCRUD(object):
         if logout_at is None:
             payload['login_at'] = datetime.datetime.now()
 
+        try:
+            from api.lib.common_setting.employee import EmployeeCRUD
+            EmployeeCRUD.update_last_login_by_uid(current_user.uid)
+        except:
+            pass
+
         return AuditLoginLog.create(**payload).id
diff --git a/cmdb-api/api/lib/perm/acl/user.py b/cmdb-api/api/lib/perm/acl/user.py
index 1d56b74..d17af58 100644
--- a/cmdb-api/api/lib/perm/acl/user.py
+++ b/cmdb-api/api/lib/perm/acl/user.py
@@ -63,6 +63,7 @@ class UserCRUD(object):
         AuditCRUD.add_role_log(None, AuditOperateType.create,
                                AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
                                )
+
         if add_from != 'common':
             from api.lib.common_setting.employee import EmployeeCRUD
             payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
diff --git a/cmdb-api/api/models/acl.py b/cmdb-api/api/models/acl.py
index d4c9efa..e3396c3 100644
--- a/cmdb-api/api/models/acl.py
+++ b/cmdb-api/api/models/acl.py
@@ -145,7 +145,7 @@ class User(CRUDModel, SoftDeleteMixin):
 class RoleQuery(BaseQuery):
 
     def authenticate(self, login, password):
-        role = self.filter(Role.name == login).first()
+        role = self.filter(Role.name == login).filter(Role.deleted.is_(False)).first()
         if role:
             authenticated = role.check_password(password)
 
diff --git a/cmdb-api/api/tasks/acl.py b/cmdb-api/api/tasks/acl.py
index 750eb2e..1e86d36 100644
--- a/cmdb-api/api/tasks/acl.py
+++ b/cmdb-api/api/tasks/acl.py
@@ -29,6 +29,7 @@ from api.models.acl import Trigger
              name="acl.role_rebuild",
              queue=ACL_QUEUE,
              once={"graceful": True, "unlock_before_run": True})
+@flush_db
 @reconnect_db
 def role_rebuild(rids, app_id):
     rids = rids if isinstance(rids, list) else [rids]
@@ -190,18 +191,18 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
 
 @celery.task(name="acl.op_record", queue=ACL_QUEUE)
 @reconnect_db
-def op_record(app, rolename, operate_type, obj):
+def op_record(app, role_name, operate_type, obj):
     if isinstance(app, int):
         app = AppCache.get(app)
         app = app and app.name
 
-    if isinstance(rolename, int):
-        u = UserCache.get(rolename)
+    if isinstance(role_name, int):
+        u = UserCache.get(role_name)
         if u:
-            rolename = u.username
+            role_name = u.username
         if not u:
-            r = RoleCache.get(rolename)
+            r = RoleCache.get(role_name)
             if r:
-                rolename = r.name
+                role_name = r.name
 
-    OperateRecordCRUD.add(app, rolename, operate_type, obj)
+    OperateRecordCRUD.add(app, role_name, operate_type, obj)
diff --git a/cmdb-api/api/views/cmdb/ci_type.py b/cmdb-api/api/views/cmdb/ci_type.py
index 1704f1b..c26d2aa 100644
--- a/cmdb-api/api/views/cmdb/ci_type.py
+++ b/cmdb-api/api/views/cmdb/ci_type.py
@@ -105,7 +105,6 @@ class CITypeGroupView(APIView):
 
         return self.jsonify(group.to_dict())
 
-    @role_required(RoleEnum.CONFIG)
     @args_validate(CITypeGroupManager.cls)
     def put(self, gid=None):
         if "/order" in request.url:
@@ -167,7 +166,8 @@ class CITypeAttributeView(APIView):
         t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found)
         type_id = t.id
         unique_id = t.unique_id
-        unique = AttributeCache.get(unique_id).name
+        unique = AttributeCache.get(unique_id)
+        unique = unique and unique.name
 
         attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
         attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id)
@@ -319,12 +319,14 @@ class CITypeAttributeGroupView(APIView):
 
 
 class CITypeTemplateView(APIView):
-    url_prefix = ("/ci_types/template/import", "/ci_types/template/export")
+    url_prefix = ("/ci_types/template/import", "/ci_types/template/export", "/ci_types/<int:type_id>/template/export")
 
     @role_required(RoleEnum.CONFIG)
-    def get(self):  # export
-        return self.jsonify(
-            dict(ci_type_template=CITypeTemplateManager.export_template()))
+    def get(self, type_id=None):  # export
+        if type_id is not None:
+            return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template_by_type(type_id)))
+
+        return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template()))
 
     @role_required(RoleEnum.CONFIG)
     def post(self):  # import
@@ -458,11 +460,10 @@ class CITypeGrantView(APIView):
         _type = CITypeCache.get(type_id)
         type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found)
         acl = ACLManager('cmdb')
-        if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and \
-                not is_app_admin('cmdb'):
+        if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
             return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
 
-        acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms)
+        acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
 
         CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
 
@@ -482,22 +483,28 @@ class CITypeRevokeView(APIView):
         _type = CITypeCache.get(type_id)
         type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found)
         acl = ACLManager('cmdb')
-        if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and \
-                not is_app_admin('cmdb'):
+        if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
             return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
 
-        acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms)
+        acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
 
+        app_id = AppCache.get('cmdb').id
+        resource = None
         if PermEnum.READ in perms:
-            CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)
+            resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)
 
-            app_id = AppCache.get('cmdb').id
             users = RoleRelationCRUD.get_users_by_rid(rid, app_id)
             for i in (users or []):
                 if i.get('role', {}).get('id') and not RoleCRUD.has_permission(
                         i.get('role').get('id'), type_name, ResourceTypeEnum.CI_TYPE, app_id, PermEnum.READ):
                     PreferenceManager.delete_by_type_id(type_id, i.get('uid'))
 
+        if not resource:
+            from api.tasks.acl import role_rebuild
+            from api.lib.perm.acl.const import ACL_QUEUE
+
+            role_rebuild.apply_async(args=(app_id, rid), queue=ACL_QUEUE)
+
         return self.jsonify(type_id=type_id, rid=rid)
 
 
@@ -507,4 +514,3 @@ class CITypeFilterPermissionView(APIView):
     @auth_with_app_token
     def get(self, type_id):
         return self.jsonify(CIFilterPermsCRUD().get(type_id))
-