Dev api 0308 (#424)

* feat(api): grant by node in relation view

* fix(api): When removing attributes, remove the unique constraint

* feat(api): grant by service tree
This commit is contained in:
pycook 2024-03-18 19:57:25 +08:00 committed by GitHub
parent 7ff309b8b8
commit 482d34993b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 457 additions and 97 deletions

View File

@ -60,6 +60,7 @@ from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete from api.tasks.cmdb import ci_relation_delete
from api.tasks.cmdb import delete_id_filter
PASSWORD_DEFAULT_SHOW = "******" PASSWORD_DEFAULT_SHOW = "******"
@ -558,6 +559,7 @@ class CIManager(object):
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id return ci_id
@ -864,6 +866,20 @@ class CIRelationManager(object):
return numfound, len(ci_ids), result return numfound, len(ci_ids), result
@staticmethod
def recursive_children(ci_id):
result = []
def _get_children(_id):
children = CIRelation.get_by(first_ci_id=_id, to_dict=False)
result.extend([i.second_ci_id for i in children])
for child in children:
_get_children(child.second_ci_id)
_get_children(ci_id)
return result
@staticmethod @staticmethod
def _sort_handler(sort_by, query_sql): def _sort_handler(sort_by, query_sql):
@ -1015,6 +1031,7 @@ class CIRelationManager(object):
his_manager.add(cr, operate_type=OperateType.DELETE) his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
return cr_id return cr_id
@ -1026,9 +1043,13 @@ class CIRelationManager(object):
to_dict=False, to_dict=False,
first=True) first=True)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) if cr is not None:
cls.delete(cr.id)
return cr and cls.delete(cr.id) ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
return cr
@classmethod @classmethod
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None): def batch_update(cls, ci_ids, parents, children, ancestor_ids=None):

View File

@ -76,6 +76,10 @@ class CITypeManager(object):
return CIType.get_by_id(ci_type.id) return CIType.get_by_id(ci_type.id)
def get_icons(self):
return {i.id: i.icon or i.name for i in db.session.query(
self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))}
@staticmethod @staticmethod
def get_ci_types(type_name=None, like=True): def get_ci_types(type_name=None, like=True):
resources = None resources = None
@ -653,6 +657,21 @@ class CITypeAttributeManager(object):
for item in PreferenceShowAttributes.get_by(type_id=type_id, attr_id=attr_id, to_dict=False): for item in PreferenceShowAttributes.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
item.soft_delete(commit=False) item.soft_delete(commit=False)
child_ids = CITypeInheritanceManager.recursive_children(type_id)
for _type_id in [type_id] + child_ids:
for item in CITypeUniqueConstraint.get_by(type_id=_type_id, to_dict=False):
if attr_id in item.attr_ids:
attr_ids = copy.deepcopy(item.attr_ids)
attr_ids.remove(attr_id)
if attr_ids:
item.update(attr_ids=attr_ids, commit=False)
else:
item.soft_delete(commit=False)
item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True)
item and item.soft_delete(commit=False)
db.session.commit() db.session.commit()
CITypeAttributeCache.clean(type_id, attr_id) CITypeAttributeCache.clean(type_id, attr_id)

View File

@ -1,12 +1,15 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy
import functools import functools
import redis_lock
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.mixin import DBMixin from api.lib.mixin import DBMixin
@ -40,6 +43,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['rid']]['ci_filter'] = "" result[i['rid']]['ci_filter'] = ""
result[i['rid']]['ci_filter'] += (i['ci_filter'] or "") result[i['rid']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['rid']]['id_filter']:
result[i['rid']]['id_filter'] = {}
result[i['rid']]['id_filter'].update(i['id_filter'] or {})
return result return result
def get_by_ids(self, _ids, type_id=None): def get_by_ids(self, _ids, type_id=None):
@ -70,6 +78,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['type_id']]['ci_filter'] = "" result[i['type_id']]['ci_filter'] = ""
result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "") result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['type_id']]['id_filter']:
result[i['type_id']]['id_filter'] = {}
result[i['type_id']]['id_filter'].update(i['id_filter'] or {})
return result return result
@classmethod @classmethod
@ -82,6 +95,54 @@ class CIFilterPermsCRUD(DBMixin):
type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id) type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id)
return type2filter_perms.get(type_id, {}).get('attr_filter') or [] return type2filter_perms.get(type_id, {}).get('attr_filter') or []
def _revoke_children(self, rid, id_filter, rebuild=True):
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
changed, item_id_filter = False, copy.deepcopy(item.id_filter)
for prefix in id_filter:
for k, v in copy.deepcopy((item.id_filter or {})).items():
if k.startswith(prefix) and k != prefix:
item_id_filter.pop(k)
changed = True
if not item_id_filter and current_app.config.get('USE_ACL'):
item.soft_delete(commit=False)
ACLManager().del_resource(str(item.id), ResourceTypeEnum.CI_FILTER, rebuild=rebuild)
elif changed:
item.update(id_filter=item_id_filter, commit=False)
db.session.commit()
def _revoke_parent(self, rid, parent_path, rebuild=True):
parent_path = [i for i in parent_path.split(',') if i] or []
revoke_nodes = [','.join(parent_path[:i]) for i in range(len(parent_path), 0, -1)]
for node_path in revoke_nodes:
delete_item, can_deleted = None, True
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
if node_path in item.id_filter:
delete_item = item
if any(filter(lambda x: x.startswith(node_path) and x != node_path, item.id_filter.keys())):
can_deleted = False
break
if can_deleted and delete_item:
id_filter = copy.deepcopy(delete_item.id_filter)
id_filter.pop(node_path)
delete_item = delete_item.update(id_filter=id_filter, filter_none=False)
if current_app.config.get('USE_ACL') and not id_filter:
ACLManager().del_resource(str(delete_item.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
delete_item.soft_delete()
items.remove(delete_item)
if rebuild:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def _can_add(self, **kwargs): def _can_add(self, **kwargs):
ci_filter = kwargs.get('ci_filter') ci_filter = kwargs.get('ci_filter')
attr_filter = kwargs.get('attr_filter') or "" attr_filter = kwargs.get('attr_filter') or ""
@ -102,36 +163,67 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **kwargs): def add(self, **kwargs):
kwargs = self._can_add(**kwargs) or kwargs kwargs = self._can_add(**kwargs) or kwargs
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
request_id_filter = {}
if kwargs.get('id_filter'):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
obj = self.cls.get_by(type_id=kwargs.get('type_id'), for _id, v in (kwargs.get('id_filter') or {}).items():
rid=kwargs.get('rid'), key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)])
first=True, to_dict=False) request_id_filter[key] = v['name']
if obj is not None:
obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete() else:
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
return obj is_recursive = kwargs.pop('is_recursive', 0)
if obj is not None:
if obj.id_filter and isinstance(kwargs.get('id_filter'), dict):
obj_id_filter = copy.deepcopy(obj.id_filter)
for k, v in request_id_filter.items():
obj_id_filter[k] = v
kwargs['id_filter'] = obj_id_filter
obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter and not obj.id_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
if not is_recursive and request_id_filter:
self._revoke_children(obj.rid, request_id_filter, rebuild=False)
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter'):
return return
obj = self.cls.create(**kwargs) else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'):
return
if current_app.config.get('USE_ACL'): if request_id_filter:
try: kwargs['id_filter'] = request_id_filter
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
return obj obj = self.cls.create(**kwargs)
if current_app.config.get('USE_ACL'): # new resource
try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
return obj
def _can_update(self, **kwargs): def _can_update(self, **kwargs):
pass pass
@ -140,19 +232,84 @@ class CIFilterPermsCRUD(DBMixin):
pass pass
def delete(self, **kwargs): def delete(self, **kwargs):
obj = self.cls.get_by(type_id=kwargs.get('type_id'), with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
rid=kwargs.get('rid'), obj = self.cls.get_by(type_id=kwargs.get('type_id'),
first=True, to_dict=False) rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
if obj is not None:
resource = None
if current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete()
return resource
def delete2(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
request_id_filter = {}
for _id, v in (kwargs.get('id_filter') or {}).items():
key = ",".join([v['parent_path']] if v.get('parent_path') else [] + [str(_id)])
request_id_filter[key] = v['name']
if obj is not None:
resource = None resource = None
if current_app.config.get('USE_ACL'): if obj is not None:
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete() id_filter = {}
for k, v in copy.deepcopy(obj.id_filter or {}).items(): # important
if k not in request_id_filter:
id_filter[k] = v
if not id_filter and current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
db.session.commit()
else:
obj.update(id_filter=id_filter)
self._revoke_children(kwargs.get('rid'), request_id_filter, rebuild=False)
self._revoke_parent(kwargs.get('rid'), kwargs.get('parent_path'))
return resource return resource
def delete_id_filter_by_ci_id(self, ci_id):
items = self.cls.get_by(ci_filter=None, attr_filter=None, to_dict=False)
rebuild_roles = set()
for item in items:
id_filter = copy.deepcopy(item.id_filter)
changed = False
for node_path in item.id_filter:
if str(ci_id) in node_path:
id_filter.pop(node_path)
changed = True
if changed:
rebuild_roles.add(item.rid)
if not id_filter:
item.soft_delete(commit=False)
else:
item.update(id_filter=id_filter, commit=False)
db.session.commit()
if rebuild_roles:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
for rid in rebuild_roles:
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None): def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None):
def decorator_has_perm(func): def decorator_has_perm(func):

View File

@ -16,10 +16,11 @@ def search(query=None,
ret_key=RetKey.NAME, ret_key=RetKey.NAME,
count=1, count=1,
sort=None, sort=None,
excludes=None): excludes=None,
use_id_filter=True):
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
s = SearchFromES(query, fl, facet, page, ret_key, count, sort) s = SearchFromES(query, fl, facet, page, ret_key, count, sort)
else: else:
s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes) s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes, use_id_filter=use_id_filter)
return s return s

View File

@ -44,7 +44,10 @@ class Search(object):
count=1, count=1,
sort=None, sort=None,
ci_ids=None, ci_ids=None,
excludes=None): excludes=None,
parent_node_perm_passed=False,
use_id_filter=False,
use_ci_filter=True):
self.orig_query = query self.orig_query = query
self.fl = fl or [] self.fl = fl or []
self.excludes = excludes or [] self.excludes = excludes or []
@ -54,9 +57,13 @@ class Search(object):
self.count = count self.count = count
self.sort = sort self.sort = sort
self.ci_ids = ci_ids or [] self.ci_ids = ci_ids or []
self.raw_ci_ids = copy.deepcopy(self.ci_ids)
self.query_sql = "" self.query_sql = ""
self.type_id_list = [] self.type_id_list = []
self.only_type_query = False self.only_type_query = False
self.parent_node_perm_passed = parent_node_perm_passed
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.valid_type_names = [] self.valid_type_names = []
self.type2filter_perms = dict() self.type2filter_perms = dict()
@ -106,7 +113,7 @@ class Search(object):
self.type_id_list.append(str(ci_type.id)) self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms: if ci_type.id in self.type2filter_perms:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter') ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter: if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = [] sub = []
ci_filter = Template(ci_filter).render(user=current_user) ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','): for i in ci_filter.split(','):
@ -122,6 +129,14 @@ class Search(object):
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter']) self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else: else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter']) self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
if not self.raw_ci_ids:
self.ci_ids = list(self.type2filter_perms[ci_type.id]['id_filter'].keys())
if self.use_id_filter and not self.ci_ids and not is_app_admin('cmdb'):
self.raw_ci_ids = [0]
else: else:
raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ)) raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ))
else: else:
@ -155,6 +170,7 @@ class Search(object):
"NOT LIKE" if is_not else "LIKE", "NOT LIKE" if is_not else "LIKE",
_v.replace("*", "%")) for _v in new_v]) _v.replace("*", "%")) for _v in new_v])
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
@ -170,6 +186,7 @@ class Search(object):
"NOT BETWEEN" if is_not else "BETWEEN", "NOT BETWEEN" if is_not else "BETWEEN",
start.replace("*", "%"), end.replace("*", "%")) start.replace("*", "%"), end.replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
@ -186,6 +203,7 @@ class Search(object):
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%")) comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
@ -197,6 +215,7 @@ class Search(object):
elif field.startswith("-"): elif field.startswith("-"):
field = field[1:] field = field[1:]
sort_type = "DESC" sort_type = "DESC"
return field, sort_type return field, sort_type
def __sort_by_id(self, sort_type, query_sql): def __sort_by_id(self, sort_type, query_sql):
@ -325,6 +344,11 @@ class Search(object):
return numfound, res return numfound, res
def __get_type2filter_perms(self):
res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
def __get_types_has_read(self): def __get_types_has_read(self):
""" """
:return: _type:(type1;type2) :return: _type:(type1;type2)
@ -334,14 +358,23 @@ class Search(object):
self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']} self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']}
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER) self.__get_type2filter_perms()
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2]))) for type_id in self.type2filter_perms:
ci_type = CITypeCache.get(type_id)
if ci_type:
if self.type2filter_perms[type_id].get('id_filter'):
if self.use_id_filter:
self.valid_type_names.add(ci_type.name)
elif self.type2filter_perms[type_id].get('ci_filter'):
if self.use_ci_filter:
self.valid_type_names.add(ci_type.name)
else:
self.valid_type_names.add(ci_type.name)
return "_type:({})".format(";".join(self.valid_type_names)) return "_type:({})".format(";".join(self.valid_type_names))
def __confirm_type_first(self, queries): def __confirm_type_first(self, queries):
has_type = False has_type = False
result = [] result = []
@ -375,7 +408,10 @@ class Search(object):
result.append(q) result.append(q)
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker" _is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
if result and not has_type and not _is_app_admin: if self.parent_node_perm_passed:
self.__get_type2filter_perms()
self.valid_type_names = "ALL"
elif result and not has_type and not _is_app_admin:
type_q = self.__get_types_has_read() type_q = self.__get_types_has_read()
if id_query: if id_query:
ci = CIManager.get_by_id(id_query) ci = CIManager.get_by_id(id_query)
@ -389,8 +425,6 @@ class Search(object):
else: else:
self.__get_types_has_read() self.__get_types_has_read()
current_app.logger.warning(result)
return result return result
def __query_by_attr(self, q, queries, alias): def __query_by_attr(self, q, queries, alias):
@ -482,7 +516,7 @@ class Search(object):
def _filter_ids(self, query_sql): def _filter_ids(self, query_sql):
if self.ci_ids: if self.ci_ids:
return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format( return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format(
query_sql, ",".join(list(map(str, self.ci_ids)))) query_sql, ",".join(list(set(map(str, self.ci_ids)))))
return query_sql return query_sql
@ -514,6 +548,9 @@ class Search(object):
s = time.time() s = time.time()
if query_sql: if query_sql:
query_sql = self._filter_ids(query_sql) query_sql = self._filter_ids(query_sql)
if self.raw_ci_ids and not self.ci_ids:
return 0, []
self.query_sql = query_sql self.query_sql = query_sql
# current_app.logger.debug(query_sql) # current_app.logger.debug(query_sql)
numfound, res = self._execute_sql(query_sql) numfound, res = self._execute_sql(query_sql)
@ -572,3 +609,8 @@ class Search(object):
total = len(response) total = len(response)
return response, counter, total, self.page, numfound, facet return response, counter, total, self.page, numfound, facet
def get_ci_ids(self):
_, ci_ids = self._query_build_raw()
return ci_ids

View File

@ -1,9 +1,11 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import json import json
import sys
from collections import Counter from collections import Counter
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user
from api.extensions import rd from api.extensions import rd
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
@ -11,11 +13,14 @@ from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci.es.search import Search as SearchFromES from api.lib.cmdb.search.ci.es.search import Search as SearchFromES
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class Search(object): class Search(object):
@ -29,7 +34,9 @@ class Search(object):
sort=None, sort=None,
reverse=False, reverse=False,
ancestor_ids=None, ancestor_ids=None,
has_m2m=None): descendant_ids=None,
has_m2m=None,
root_parent_path=None):
self.orig_query = query self.orig_query = query
self.fl = fl self.fl = fl
self.facet_field = facet_field self.facet_field = facet_field
@ -46,6 +53,8 @@ class Search(object):
level[0] if isinstance(level, list) and level else level) level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids self.ancestor_ids = ancestor_ids
self.descendant_ids = descendant_ids
self.root_parent_path = root_parent_path
self.has_m2m = has_m2m or False self.has_m2m = has_m2m or False
if not self.has_m2m: if not self.has_m2m:
if self.ancestor_ids: if self.ancestor_ids:
@ -56,27 +65,21 @@ class Search(object):
if _l < int(level) and c == ConstraintEnum.Many2Many: if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True self.has_m2m = True
self.type2filter_perms = None
def _get_ids(self, ids): def _get_ids(self, ids):
if self.level[-1] == 1 and len(ids) == 1:
if self.ancestor_ids is None:
return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)]
else:
seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0],
ancestor_ids=self.ancestor_ids,
to_dict=False)}
return list(seconds)
merge_ids = [] merge_ids = []
key = [] key = []
_tmp = [] _tmp = []
for level in range(1, sorted(self.level)[-1] + 1): for level in range(1, sorted(self.level)[-1] + 1):
if len(self.descendant_ids) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]])
else:
id_filter_limit = {}
if not self.has_m2m: if not self.has_m2m:
_tmp = map(lambda x: json.loads(x).keys(), key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or []))
ids = [j for i in _tmp for j in i]
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else: else:
if not self.ancestor_ids: if not self.ancestor_ids:
@ -92,12 +95,16 @@ class Search(object):
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]])) key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2 prefix = REDIS_PREFIX_CI_RELATION2
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or [])) if not key or id_filter_limit is None:
ids = [j for i in _tmp for j in i]
if not key:
return [] return []
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = [[i[0] for i in x if (not id_filter_limit or (
key[idx] not in id_filter_limit or int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
ids = [j for i in _tmp for j in i]
if level in self.level: if level in self.level:
merge_ids.extend(ids) merge_ids.extend(ids)
@ -120,7 +127,28 @@ class Search(object):
return merge_ids return merge_ids
def _has_read_perm_from_parent_nodes(self):
self.root_parent_path = list(map(str, self.root_parent_path))
if str(self.root_id).isdigit() and str(self.root_id) not in self.root_parent_path:
self.root_parent_path.append(str(self.root_id))
self.root_parent_path = set(self.root_parent_path)
if is_app_admin('cmdb'):
self.type2filter_perms = {}
return True
res = ACLManager().get_resources(ResourceTypeEnum.CI_FILTER) or {}
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res]))) or {}
for _, filters in self.type2filter_perms.items():
if set((filters.get('id_filter') or {}).keys()) & self.root_parent_path:
return True
return True
def search(self): def search(self):
use_ci_filter = len(self.descendant_ids) == self.level[0] - 1
parent_node_perm_passed = self._has_read_perm_from_parent_nodes()
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids] cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
@ -161,42 +189,103 @@ class Search(object):
page=self.page, page=self.page,
count=self.count, count=self.count,
sort=self.sort, sort=self.sort,
ci_ids=merge_ids).search() ci_ids=merge_ids,
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter).search()
def _get_ci_filter(self, filter_perms, ci_filters=None):
ci_filters = ci_filters or []
if ci_filters:
result = {}
for item in ci_filters:
res = SearchFromDB('_type:{},{}'.format(item['type_id'], item['ci_filter']),
count=sys.maxsize, parent_node_perm_passed=True).get_ci_ids()
if res:
result[item['type_id']] = set(res)
return {}, result if result else None
result = dict()
if filter_perms.get('id_filter'):
for k in filter_perms['id_filter']:
node_path = k.split(',')
if len(node_path) == 1:
result[int(node_path[0])] = 1
elif not self.has_m2m:
result.setdefault(node_path[-2], set()).add(int(node_path[-1]))
else:
result.setdefault(','.join(node_path[:-1]), set()).add(int(node_path[-1]))
if result:
return result, None
else:
return None, None
return {}, None
def statistics(self, type_ids): def statistics(self, type_ids):
self.level = int(self.level) self.level = int(self.level)
acl = ACLManager('cmdb')
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
type2filter_perms = dict()
if not _is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp = [] _tmp = []
level2ids = {} level2ids = {}
for lv in range(1, self.level + 1): for lv in range(1, self.level + 1):
level2ids[lv] = [] level2ids[lv] = []
id_filter_limit, ci_filter_limit = None, None
if len(self.descendant_ids) >= lv and type2filter_perms.get(self.descendant_ids[lv - 1]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[self.descendant_ids[lv - 1]])
elif type_ids and self.level == lv:
ci_filters = [type2filter_perms[type_id] for type_id in type_ids if type_id in type2filter_perms]
if ci_filters:
id_filter_limit, ci_filter_limit = self._get_ci_filter({}, ci_filters=ci_filters)
else:
id_filter_limit = {}
else:
id_filter_limit = {}
if lv == 1: if lv == 1:
if not self.has_m2m: if not self.has_m2m:
key, prefix = ids, REDIS_PREFIX_CI_RELATION key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else: else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
if not self.ancestor_ids: if not self.ancestor_ids:
key, prefix = ids, REDIS_PREFIX_CI_RELATION key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else: else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
prefix = REDIS_PREFIX_CI_RELATION2 prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key] level2ids[lv] = [[i] for i in key]
if not key: if not key or id_filter_limit is None:
_tmp = [] _tmp = [[]] * len(ids)
continue continue
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = []
if type_ids and lv == self.level: if type_ids and lv == self.level:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids], _tmp = [[i for i in x if i[1] in type_ids and
(map(lambda x: list(json.loads(x).items()), (not id_filter_limit or (key[idx] not in id_filter_limit or
[i or '{}' for i in rd.get(key, prefix) or []])))) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else: else:
_tmp = list(map(lambda x: list(json.loads(x).items()), _tmp = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
[i or '{}' for i in rd.get(key, prefix) or []])) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
if ci_filter_limit:
_tmp = [[j for j in i if j[1] not in ci_filter_limit or int(j[0]) in ci_filter_limit[j[1]]]
for i in _tmp]
else: else:
for idx, item in enumerate(_tmp): for idx, item in enumerate(_tmp):
if item: if item:
if not self.has_m2m: if not self.has_m2m:
@ -208,15 +297,22 @@ class Search(object):
level2ids[lv].append(key) level2ids[lv].append(key)
if key: if key:
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
if type_ids and lv == self.level: if type_ids and lv == self.level:
__tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() __tmp = [[i for i in x if i[1] in type_ids and
if type_id in type_ids], (not id_filter_limit or (
filter(lambda x: x is not None, key[idx] not in id_filter_limit or
rd.get(key, prefix) or [])) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else: else:
__tmp = map(lambda x: list(json.loads(x).items()), __tmp = [[i for i in x if (not id_filter_limit or (
filter(lambda x: x is not None, key[idx] not in id_filter_limit or
rd.get(key, prefix) or [])) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
if ci_filter_limit:
__tmp = [[j for j in i if j[1] not in ci_filter_limit or
int(j[0]) in ci_filter_limit[j[1]]] for i in __tmp]
else: else:
__tmp = [] __tmp = []

View File

@ -569,6 +569,7 @@ class CIFilterPerms(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id')) type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
ci_filter = db.Column(db.Text) ci_filter = db.Column(db.Text)
attr_filter = db.Column(db.Text) attr_filter = db.Column(db.Text)
id_filter = db.Column(db.JSON) # {node_path: unique_value}
rid = db.Column(db.Integer, index=True) rid = db.Column(db.Integer, index=True)

View File

@ -18,6 +18,7 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.decorator import flush_db from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
@ -83,6 +84,13 @@ def ci_delete(ci_id):
current_app.logger.info("{0} delete..........".format(ci_id)) current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.delete_id_filter", queue=CMDB_QUEUE)
@reconnect_db
def delete_id_filter(ci_id):
CIFilterPermsCRUD().delete_id_filter_by_ci_id(ci_id)
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict): def ci_delete_trigger(trigger, operate_type, ci_dict):

View File

@ -11,8 +11,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
@ -152,9 +151,10 @@ class CISearchView(APIView):
ret_key = RetKey.NAME ret_key = RetKey.NAME
facet = handle_arg_list(request.values.get("facet", "")) facet = handle_arg_list(request.values.get("facet", ""))
sort = request.values.get("sort") sort = request.values.get("sort")
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes) s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try: try:
response, counter, total, page, numfound, facet = s.search() response, counter, total, page, numfound, facet = s.search()
except SearchError as e: except SearchError as e:

View File

@ -13,7 +13,6 @@ from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci_relation.search import Search from api.lib.cmdb.search.ci_relation.search import Search
from api.lib.decorator import args_required from api.lib.decorator import args_required
from api.lib.perm.auth import auth_abandoned
from api.lib.utils import get_page from api.lib.utils import get_page
from api.lib.utils import get_page_size from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
@ -36,6 +35,8 @@ class CIRelationSearchView(APIView):
root_id = request.values.get('root_id') root_id = request.values.get('root_id')
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
root_parent_path = handle_arg_list(request.values.get('root_parent_path') or '')
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
level = list(map(int, handle_arg_list(request.values.get('level', '1')))) level = list(map(int, handle_arg_list(request.values.get('level', '1'))))
query = request.values.get('q', "") query = request.values.get('q', "")
@ -47,7 +48,8 @@ class CIRelationSearchView(APIView):
start = time.time() start = time.time()
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, s = Search(root_id, level, query, fl, facet, page, count, sort, reverse,
ancestor_ids=ancestor_ids, has_m2m=has_m2m) ancestor_ids=ancestor_ids, has_m2m=has_m2m, root_parent_path=root_parent_path,
descendant_ids=descendant_ids)
try: try:
response, counter, total, page, numfound, facet = s.search() response, counter, total, page, numfound, facet = s.search()
except SearchError as e: except SearchError as e:
@ -65,16 +67,16 @@ class CIRelationSearchView(APIView):
class CIRelationStatisticsView(APIView): class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics" url_prefix = "/ci_relations/statistics"
@auth_abandoned
def get(self): def get(self):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids')))) root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1) level = request.values.get('level', 1)
type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', [])))) type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', []))))
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE') has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = Search(root_ids, level, ancestor_ids=ancestor_ids, has_m2m=has_m2m) s = Search(root_ids, level, ancestor_ids=ancestor_ids, descendant_ids=descendant_ids, has_m2m=has_m2m)
try: try:
result = s.statistics(type_ids) result = s.statistics(type_ids)
except SearchError as e: except SearchError as e:

View File

@ -38,9 +38,13 @@ from api.resource import APIView
class CITypeView(APIView): class CITypeView(APIView):
url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>") url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>",
"/ci_types/icons")
def get(self, type_id=None, type_name=None): def get(self, type_id=None, type_name=None):
if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name") q = request.args.get("type_name")
if type_id is not None: if type_id is not None:
@ -490,13 +494,14 @@ class CITypeGrantView(APIView):
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)) 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, rebuild=False) if perms and not request.values.get('id_filter'):
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
resource = None new_resource = None
if 'ci_filter' in request.values or 'attr_filter' in request.values: if 'ci_filter' in request.values or 'attr_filter' in request.values or 'id_filter' in request.values:
resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values) new_resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
if not resource: if not new_resource:
from api.tasks.acl import role_rebuild from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE from api.lib.perm.acl.const import ACL_QUEUE
@ -522,10 +527,18 @@ class CITypeRevokeView(APIView):
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)) 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, rebuild=False)
app_id = AppCache.get('cmdb').id app_id = AppCache.get('cmdb').id
resource = None resource = None
if request.values.get('id_filter'):
CIFilterPermsCRUD().delete2(
type_id=type_id, rid=rid, id_filter=request.values['id_filter'],
parent_path=request.values.get('parent_path'))
return self.jsonify(type_id=type_id, rid=rid)
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
if PermEnum.READ in perms or not perms: if PermEnum.READ in perms or not perms:
resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid) resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)