diff --git a/.env b/.env index 77b92e1a..e25dee4b 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ - MYSQL_ROOT_PASSWORD='123456' + MYSQL_ROOT_PASSWORD='change_me_strong_root_password' MYSQL_HOST='mysql' MYSQL_PORT=3306 MYSQL_USER='cmdb' MYSQL_DATABASE='cmdb' - MYSQL_PASSWORD='123456' + MYSQL_PASSWORD='change_me_strong_db_password' diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 2dde2719..4b4ba489 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -6,25 +6,25 @@ name = "pypi" [packages] # Flask Flask = "==2.2.5" -Werkzeug = "==2.2.3" +Werkzeug = "==2.3.8" click = ">=5.0" # Api Flask-RESTful = "==0.3.10" # Database Flask-SQLAlchemy = "==3.0.5" SQLAlchemy = "==1.4.49" -PyMySQL = "==1.1.0" +PyMySQL = "==1.1.1" redis = "==4.6.0" python-redis-lock = "==4.0.0" # Migrations Flask-Migrate = "==2.5.2" # Deployment -gunicorn = "==21.0.1" +gunicorn = "==22.0.0" supervisor = "==4.0.3" # Auth Flask-Login = ">=0.6.2" Flask-Bcrypt = "==1.0.1" -Flask-Cors = ">=3.0.8" +Flask-Cors = "==4.0.2" ldap3 = "==2.9.1" pycryptodome = "==3.12.0" cryptography = ">=41.0.2" @@ -51,14 +51,14 @@ Pillow = ">=10.0.1" six = "==1.16.0" bs4 = ">=0.0.1" toposort = ">=1.5" -requests = ">=2.22.0" +requests = "==2.32.4" requests_oauthlib = "==1.3.1" -markdownify = "==0.11.6" +markdownify = "==0.14.1" PyJWT = "==2.4.0" elasticsearch = "==7.17.9" future = "==0.18.3" itsdangerous = "==2.1.2" -Jinja2 = "==3.1.2" +Jinja2 = "==3.1.6" jinja2schema = "==0.1.4" msgpack-python = "==0.5.6" alembic = "==1.7.7" diff --git a/cmdb-api/api/commands/click_cmdb.py b/cmdb-api/api/commands/click_cmdb.py index 7794a293..f9bacd5a 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -16,6 +16,11 @@ from api.extensions import db from api.extensions import rd from api.lib.cmdb.cache import AttributeCache +from api.lib.cmdb.cache import CITypeAttributesCache +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.cache import RelationTypeCache +from api.lib.cmdb.const import BuiltinModelEnum +from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION @@ -41,11 +46,16 @@ from api.models.cmdb import CI from api.models.cmdb import CIRelation from api.models.cmdb import CIType +from api.models.cmdb import CITypeAttribute +from api.models.cmdb import CITypeRelation from api.models.cmdb import CITypeTrigger from api.models.cmdb import OperationRecord from api.models.cmdb import PreferenceRelationView +from api.models.cmdb import RelationType from api.tasks.cmdb import batch_ci_cache +INNER_SECRET_HTTP_TIMEOUT = 5 + @click.command() @with_appcontext @@ -367,7 +377,8 @@ def cmdb_inner_secrets_init(address): token = click.prompt('Enter root token', hide_input=True, confirmation_prompt=False) assert token is not None resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")), - headers={"Inner-Token": token}) + headers={"Inner-Token": token}, + timeout=INNER_SECRET_HTTP_TIMEOUT) if resp.status_code == 200: KeyManage.print_response(resp.json()) else: @@ -430,7 +441,7 @@ def cmdb_inner_secrets_seal(address, token): address = "{}/api/v0.1/secrets/seal".format(address.strip("/")) resp = requests.post(address, headers={ "Inner-Token": token, - }) + }, timeout=INNER_SECRET_HTTP_TIMEOUT) if resp.status_code == 200: KeyManage.print_response(resp.json()) else: @@ -523,6 +534,230 @@ def cmdb_agent_init(): click.echo("Secret: {}".format(click.style(user.secret, bg='red'))) +@click.command() +@click.option( + '--repair-existing', + is_flag=True, + default=False, + help='Repair existing built-in model definitions if field/type mappings are outdated', +) +@with_appcontext +def cmdb_bootstrap_builtin_models(repair_existing): + """ + Bootstrap minimal built-in models for DCIM/IPAM pages. + """ + summary = { + "attrs_created": 0, + "attrs_updated": 0, + "types_created": 0, + "types_updated": 0, + "type_attrs_created": 0, + "type_attrs_updated": 0, + "type_relations_created": 0, + "relation_types_created": 0, + } + + def ensure_attr(name, alias, value_type, **options): + existed = Attribute.get_by(name=name, first=True, to_dict=False) + if existed is None: + attr = Attribute.create(name=name, alias=alias, value_type=value_type, uid=0, **options) + summary["attrs_created"] += 1 + return attr + + if existed.value_type != value_type: + raise click.ClickException( + f"Existing attribute '{name}' has value_type={existed.value_type}, " + f"expected={value_type}. Please fix it manually before bootstrap." + ) + + if repair_existing: + updates = {} + if alias and existed.alias != alias: + updates["alias"] = alias + for key, value in options.items(): + if getattr(existed, key) != value: + updates[key] = value + if updates: + existed.update(**updates) + summary["attrs_updated"] += 1 + + return existed + + def ensure_type(name, alias, unique_attr, show_attr=None): + show_attr = show_attr or unique_attr + existed = CIType.get_by(name=name, first=True, to_dict=False) + if existed is None: + ci_type = CIType.create(name=name, alias=alias, unique_id=unique_attr.id, show_id=show_attr.id, uid=0) + summary["types_created"] += 1 + return ci_type + + if repair_existing: + updates = {} + if existed.unique_id != unique_attr.id: + updates["unique_id"] = unique_attr.id + if existed.show_id != show_attr.id: + updates["show_id"] = show_attr.id + if alias and existed.alias != alias: + updates["alias"] = alias + if updates: + existed.update(**updates) + summary["types_updated"] += 1 + + return existed + + def ensure_type_attr(ci_type, attr, is_required=False, default_show=True, order=0): + existed = CITypeAttribute.get_by(type_id=ci_type.id, attr_id=attr.id, first=True, to_dict=False) + if existed is None: + CITypeAttribute.create( + type_id=ci_type.id, + attr_id=attr.id, + is_required=is_required, + default_show=default_show, + order=order, + ) + summary["type_attrs_created"] += 1 + return + + if repair_existing: + updates = {} + if existed.is_required != is_required: + updates["is_required"] = is_required + if existed.default_show != default_show: + updates["default_show"] = default_show + if updates: + existed.update(**updates) + summary["type_attrs_updated"] += 1 + + def ensure_relation_type(): + relation_type = RelationType.get_by(first=True, to_dict=False) + if relation_type is not None: + return relation_type + + relation_type = RelationType.create(name="connect") + summary["relation_types_created"] += 1 + return relation_type + + def ensure_type_relation(parent_type, child_type, relation_type_id): + existed = CITypeRelation.get_by( + parent_id=parent_type.id, child_id=child_type.id, first=True, to_dict=False + ) + if existed is not None: + if repair_existing and existed.relation_type_id != relation_type_id: + existed.update(relation_type_id=relation_type_id, constraint=ConstraintEnum.One2Many) + return + + CITypeRelation.create( + parent_id=parent_type.id, + child_id=child_type.id, + relation_type_id=relation_type_id, + constraint=ConstraintEnum.One2Many, + ) + summary["type_relations_created"] += 1 + + try: + attrs = {} + attrs["name"] = ensure_attr("name", "Name", ValueTypeEnum.TEXT, is_index=True) + attrs["cidr"] = ensure_attr("cidr", "CIDR", ValueTypeEnum.TEXT, is_index=True) + attrs["hosts_count"] = ensure_attr("hosts_count", "Hosts Count", ValueTypeEnum.INT) + attrs["assign_count"] = ensure_attr("assign_count", "Assigned Count", ValueTypeEnum.INT) + attrs["used_count"] = ensure_attr("used_count", "Used Count", ValueTypeEnum.INT) + attrs["free_count"] = ensure_attr("free_count", "Free Count", ValueTypeEnum.INT) + attrs["ip"] = ensure_attr("ip", "IP", ValueTypeEnum.TEXT, is_index=True) + attrs["assign_status"] = ensure_attr("assign_status", "Assign Status", ValueTypeEnum.INT) + attrs["is_used"] = ensure_attr("is_used", "Is Used", ValueTypeEnum.BOOL, is_bool=True) + attrs["u_count"] = ensure_attr("u_count", "U Count", ValueTypeEnum.INT) + attrs["free_u_count"] = ensure_attr("free_u_count", "Free U Count", ValueTypeEnum.INT) + attrs["u_slot_abnormal"] = ensure_attr( + "u_slot_abnormal", "U Slot Abnormal", ValueTypeEnum.BOOL, is_bool=True + ) + + types = {} + types[BuiltinModelEnum.DCIM_REGION] = ensure_type( + BuiltinModelEnum.DCIM_REGION, "DCIM Region", attrs["name"], attrs["name"] + ) + types[BuiltinModelEnum.DCIM_IDC] = ensure_type( + BuiltinModelEnum.DCIM_IDC, "DCIM IDC", attrs["name"], attrs["name"] + ) + types[BuiltinModelEnum.DCIM_SERVER_ROOM] = ensure_type( + BuiltinModelEnum.DCIM_SERVER_ROOM, "DCIM Server Room", attrs["name"], attrs["name"] + ) + types[BuiltinModelEnum.DCIM_RACK] = ensure_type( + BuiltinModelEnum.DCIM_RACK, "DCIM Rack", attrs["name"], attrs["name"] + ) + types[BuiltinModelEnum.IPAM_SCOPE] = ensure_type( + BuiltinModelEnum.IPAM_SCOPE, "IPAM Scope", attrs["name"], attrs["name"] + ) + types[BuiltinModelEnum.IPAM_SUBNET] = ensure_type( + BuiltinModelEnum.IPAM_SUBNET, "IPAM Subnet", attrs["cidr"], attrs["name"] + ) + types[BuiltinModelEnum.IPAM_ADDRESS] = ensure_type( + BuiltinModelEnum.IPAM_ADDRESS, "IPAM Address", attrs["ip"], attrs["ip"] + ) + + ensure_type_attr(types[BuiltinModelEnum.DCIM_REGION], attrs["name"], is_required=True) + ensure_type_attr(types[BuiltinModelEnum.DCIM_IDC], attrs["name"], is_required=True) + ensure_type_attr(types[BuiltinModelEnum.DCIM_SERVER_ROOM], attrs["name"], is_required=True) + + ensure_type_attr(types[BuiltinModelEnum.DCIM_RACK], attrs["name"], is_required=True) + ensure_type_attr(types[BuiltinModelEnum.DCIM_RACK], attrs["u_count"], is_required=True) + ensure_type_attr(types[BuiltinModelEnum.DCIM_RACK], attrs["free_u_count"]) + ensure_type_attr(types[BuiltinModelEnum.DCIM_RACK], attrs["u_slot_abnormal"]) + + ensure_type_attr(types[BuiltinModelEnum.IPAM_SCOPE], attrs["name"], is_required=True) + + ensure_type_attr(types[BuiltinModelEnum.IPAM_SUBNET], attrs["name"]) + ensure_type_attr(types[BuiltinModelEnum.IPAM_SUBNET], attrs["cidr"], is_required=True) + ensure_type_attr(types[BuiltinModelEnum.IPAM_SUBNET], attrs["hosts_count"]) + ensure_type_attr(types[BuiltinModelEnum.IPAM_SUBNET], attrs["assign_count"]) + ensure_type_attr(types[BuiltinModelEnum.IPAM_SUBNET], attrs["used_count"]) + ensure_type_attr(types[BuiltinModelEnum.IPAM_SUBNET], attrs["free_count"]) + + ensure_type_attr(types[BuiltinModelEnum.IPAM_ADDRESS], attrs["name"]) + ensure_type_attr(types[BuiltinModelEnum.IPAM_ADDRESS], attrs["ip"], is_required=True) + ensure_type_attr(types[BuiltinModelEnum.IPAM_ADDRESS], attrs["assign_status"]) + ensure_type_attr(types[BuiltinModelEnum.IPAM_ADDRESS], attrs["is_used"]) + + relation_type = ensure_relation_type() + ensure_type_relation( + types[BuiltinModelEnum.DCIM_REGION], types[BuiltinModelEnum.DCIM_IDC], relation_type.id + ) + ensure_type_relation( + types[BuiltinModelEnum.DCIM_IDC], types[BuiltinModelEnum.DCIM_SERVER_ROOM], relation_type.id + ) + ensure_type_relation( + types[BuiltinModelEnum.DCIM_SERVER_ROOM], types[BuiltinModelEnum.DCIM_RACK], relation_type.id + ) + ensure_type_relation( + types[BuiltinModelEnum.IPAM_SCOPE], types[BuiltinModelEnum.IPAM_SCOPE], relation_type.id + ) + ensure_type_relation( + types[BuiltinModelEnum.IPAM_SCOPE], types[BuiltinModelEnum.IPAM_SUBNET], relation_type.id + ) + ensure_type_relation( + types[BuiltinModelEnum.IPAM_SUBNET], types[BuiltinModelEnum.IPAM_SUBNET], relation_type.id + ) + ensure_type_relation( + types[BuiltinModelEnum.IPAM_SUBNET], types[BuiltinModelEnum.IPAM_ADDRESS], relation_type.id + ) + + for attr in attrs.values(): + AttributeCache.clean(attr) + for ci_type in types.values(): + CITypeAttributesCache.clean(ci_type.id) + CITypeCache.clean(ci_type.id) + RelationTypeCache.clean(relation_type.id) + + except Exception as e: + db.session.rollback() + raise click.ClickException(f"cmdb bootstrap built-in models failed: {e}") + + click.echo("CMDB built-in models bootstrap completed.") + click.echo(json.dumps(summary, ensure_ascii=False)) + click.echo("Next recommended commands:") + click.echo(" flask cmdb-init-acl") + click.echo(" flask cmdb-init-cache") + + @click.command() @click.option( '-v', diff --git a/cmdb-api/api/commands/common.py b/cmdb-api/api/commands/common.py index c7ccf255..4249c42c 100644 --- a/cmdb-api/api/commands/common.py +++ b/cmdb-api/api/commands/common.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Click commands.""" import os +import re from glob import glob from subprocess import call @@ -12,6 +13,7 @@ HERE = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.join(HERE, os.pardir, os.pardir) TEST_PATH = os.path.join(PROJECT_ROOT, "tests") +LANG_CODE_RE = re.compile(r"^[A-Za-z0-9_.@-]+$") @click.command() @@ -108,16 +110,22 @@ def translate(): """Translation and localization commands.""" +def _run_translate_command(command, err): + if call(command): + raise RuntimeError(err) + + @translate.command() @click.argument('lang') def init(lang): """Initialize a new language.""" + if not LANG_CODE_RE.fullmatch(lang): + raise click.BadParameter("Invalid language code", param_hint="lang") - if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): - raise RuntimeError('extract command failed') - if os.system( - 'pybabel init -i messages.pot -d api/translations -l ' + lang): - raise RuntimeError('init command failed') + _run_translate_command(['pybabel', 'extract', '-F', 'babel.cfg', '-k', '_l', '-o', 'messages.pot', '.'], + 'extract command failed') + _run_translate_command(['pybabel', 'init', '-i', 'messages.pot', '-d', 'api/translations', '-l', lang], + 'init command failed') os.remove('messages.pot') @@ -125,10 +133,10 @@ def init(lang): def update(): """Update all languages.""" - if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): - raise RuntimeError('extract command failed') - if os.system('pybabel update -i messages.pot -d api/translations'): - raise RuntimeError('update command failed') + _run_translate_command(['pybabel', 'extract', '-F', 'babel.cfg', '-k', '_l', '-o', 'messages.pot', '.'], + 'extract command failed') + _run_translate_command(['pybabel', 'update', '-i', 'messages.pot', '-d', 'api/translations'], + 'update command failed') os.remove('messages.pot') @@ -136,5 +144,4 @@ def update(): def compile(): """Compile all languages.""" - if os.system('pybabel compile -d api/translations'): - raise RuntimeError('compile command failed') + _run_translate_command(['pybabel', 'compile', '-d', 'api/translations'], 'compile command failed') diff --git a/cmdb-api/api/lib/cmdb/attribute.py b/cmdb-api/api/lib/cmdb/attribute.py index 77175b01..df1804cf 100644 --- a/cmdb-api/api/lib/cmdb/attribute.py +++ b/cmdb-api/api/lib/cmdb/attribute.py @@ -18,6 +18,8 @@ from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import CITypeHistoryManager from api.lib.cmdb.resp_format import ErrFormat +from api.lib.cmdb.safe_script import UnsafeScriptError +from api.lib.cmdb.safe_script import load_class_from_script from api.lib.cmdb.utils import ValueTypeMap from api.lib.decorator import kwargs_required from api.lib.perm.acl.acl import is_app_admin @@ -80,11 +82,12 @@ def _get_choice_values_from_other(choice_other): elif choice_other.get('script'): try: - x = compile(choice_other['script'], '', "exec") - local_ns = {} - exec(x, {}, local_ns) - res = local_ns['ChoiceValue']().values() or [] + choice_cls = load_class_from_script(choice_other['script'], 'ChoiceValue') + res = choice_cls().values() or [] return [[i, {}] for i in res] + except UnsafeScriptError as e: + current_app.logger.error("get choice values from script: {}".format(e)) + return [] except Exception as e: current_app.logger.error("get choice values from script: {}".format(e)) return [] diff --git a/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py b/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py index 99201eed..6cfc122c 100644 --- a/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py +++ b/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py @@ -26,6 +26,8 @@ from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.custom_dashboard import SystemConfigManager from api.lib.cmdb.resp_format import ErrFormat +from api.lib.cmdb.safe_script import UnsafeScriptError +from api.lib.cmdb.safe_script import load_class_from_script from api.lib.cmdb.search import SearchError from api.lib.cmdb.search.ci import search as ci_search from api.lib.common_setting.role_perm_base import CMDBApp @@ -52,11 +54,12 @@ def parse_plugin_script(script): attributes = [] try: - x = compile(script, '', "exec") - local_ns = {} - exec(x, {}, local_ns) - unique_key = local_ns['AutoDiscovery']().unique_key - attrs = local_ns['AutoDiscovery']().attributes() or [] + plugin_cls = load_class_from_script(script, 'AutoDiscovery') + plugin = plugin_cls() + unique_key = plugin.unique_key + attrs = plugin.attributes() or [] + except UnsafeScriptError as e: + return abort(400, str(e)) except Exception as e: return abort(400, str(e)) diff --git a/cmdb-api/api/lib/cmdb/safe_script.py b/cmdb-api/api/lib/cmdb/safe_script.py new file mode 100644 index 00000000..e16a0075 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/safe_script.py @@ -0,0 +1,118 @@ +# -*- coding:utf-8 -*- + +import ast +import builtins + + +class UnsafeScriptError(Exception): + pass + + +class _RestrictedScriptChecker(ast.NodeVisitor): + FORBIDDEN_NODES = ( + ast.Import, + ast.ImportFrom, + ast.Global, + ast.Nonlocal, + ast.AsyncFunctionDef, + ast.Await, + ast.Yield, + ast.YieldFrom, + ast.Lambda, + ast.With, + ast.AsyncWith, + ast.Delete, + ) + FORBIDDEN_NAMES = { + "__import__", + "eval", + "exec", + "open", + "compile", + "input", + "globals", + "locals", + "vars", + "dir", + "getattr", + "setattr", + "delattr", + "help", + "breakpoint", + } + + def visit(self, node): + if isinstance(node, self.FORBIDDEN_NODES): + raise UnsafeScriptError("forbidden syntax: {0}".format(node.__class__.__name__)) + return super(_RestrictedScriptChecker, self).visit(node) + + def visit_Name(self, node): + if node.id.startswith("__") or node.id in self.FORBIDDEN_NAMES: + raise UnsafeScriptError("forbidden name: {0}".format(node.id)) + return self.generic_visit(node) + + def visit_Attribute(self, node): + if node.attr.startswith("__"): + raise UnsafeScriptError("forbidden attribute access: {0}".format(node.attr)) + return self.generic_visit(node) + + def visit_Call(self, node): + if isinstance(node.func, ast.Name): + if node.func.id.startswith("__") or node.func.id in self.FORBIDDEN_NAMES: + raise UnsafeScriptError("forbidden function call: {0}".format(node.func.id)) + elif isinstance(node.func, ast.Attribute): + if node.func.attr.startswith("__"): + raise UnsafeScriptError("forbidden function call: {0}".format(node.func.attr)) + return self.generic_visit(node) + + +_ALLOWED_BUILTINS = { + "__build_class__": builtins.__build_class__, + "object": object, + "Exception": Exception, + "str": str, + "int": int, + "float": float, + "bool": bool, + "dict": dict, + "list": list, + "set": set, + "tuple": tuple, + "len": len, + "range": range, + "enumerate": enumerate, + "zip": zip, + "min": min, + "max": max, + "sum": sum, + "abs": abs, + "sorted": sorted, + "all": all, + "any": any, +} + + +def load_class_from_script(script, class_name): + if not isinstance(script, str): + raise UnsafeScriptError("script must be a string") + + try: + tree = ast.parse(script, mode='exec') + except Exception as e: + raise UnsafeScriptError("invalid script: {0}".format(e)) + + _RestrictedScriptChecker().visit(tree) + code = compile(tree, '', "exec") + + local_ns = {} + global_ns = { + "__builtins__": _ALLOWED_BUILTINS, + "__name__": "__cmdb_safe_script__", + } + exec(code, global_ns, local_ns) + + klass = local_ns.get(class_name) or global_ns.get(class_name) + if klass is None: + raise UnsafeScriptError("class {0} is required".format(class_name)) + + return klass diff --git a/cmdb-api/api/lib/common_setting/employee.py b/cmdb-api/api/lib/common_setting/employee.py index 5dbeeb2b..f30ef46e 100644 --- a/cmdb-api/api/lib/common_setting/employee.py +++ b/cmdb-api/api/lib/common_setting/employee.py @@ -22,6 +22,8 @@ from api.tasks.common_setting import refresh_employee_acl_info, edit_employee_department_in_acl +EMPLOYEE_HTTP_TIMEOUT = 5 + acl_user_columns = [ 'email', 'mobile', @@ -529,7 +531,7 @@ def bind_notice_by_uid(_platform, _uid): phone=mobile, sender=_platform ) - res = requests.post(url, json=payload) + res = requests.post(url, json=payload, timeout=EMPLOYEE_HTTP_TIMEOUT) result = res.json() if res.status_code != 200: raise Exception(result.get('msg', '')) diff --git a/cmdb-api/api/lib/common_setting/notice_config.py b/cmdb-api/api/lib/common_setting/notice_config.py index 62469626..6a30928c 100644 --- a/cmdb-api/api/lib/common_setting/notice_config.py +++ b/cmdb-api/api/lib/common_setting/notice_config.py @@ -8,6 +8,8 @@ from wtforms import validators from flask import abort, current_app +NOTICE_HTTP_TIMEOUT = 5 + class NoticeConfigCRUD(object): @@ -123,7 +125,7 @@ def test_send_email(receive_address, **kwargs): "tos": [recipient_email], } current_app.logger.info(f"test_send_email: {url}, {payload}") - response = requests.post(url, json=payload) + response = requests.post(url, json=payload, timeout=NOTICE_HTTP_TIMEOUT) if response.status_code != 200: abort(400, response.text) diff --git a/cmdb-api/api/lib/http_cli.py b/cmdb-api/api/lib/http_cli.py index 9fcdd280..512e3a6e 100644 --- a/cmdb-api/api/lib/http_cli.py +++ b/cmdb-api/api/lib/http_cli.py @@ -9,6 +9,8 @@ from flask_login import current_user from future.moves.urllib.parse import urlparse +API_REQUEST_TIMEOUT = 5 + def build_api_key(path, params): current_user is not None or abort(403, u"您得登陆才能进行该操作") @@ -17,7 +19,7 @@ def build_api_key(path, params): values = "".join([str(params[k]) for k in sorted(params.keys()) if params[k] is not None]) if params.keys() else "" _secret = "".join([path, secret, values]).encode("utf-8") - params["_secret"] = hashlib.sha1(_secret).hexdigest() + params["_secret"] = hashlib.sha256(_secret).hexdigest() params["_key"] = key return params @@ -30,9 +32,9 @@ def api_request(url, method="get", params=None, ret_key=None): method = method.lower() params = build_api_key(urlparse(url).path, params) if method == "get": - resp = getattr(requests, method)(url, params=params) + resp = getattr(requests, method)(url, params=params, timeout=API_REQUEST_TIMEOUT) else: - resp = getattr(requests, method)(url, data=params) + resp = getattr(requests, method)(url, data=params, timeout=API_REQUEST_TIMEOUT) if resp.status_code != 200: return abort(resp.status_code, resp.json().get("message")) resp = resp.json() diff --git a/cmdb-api/api/lib/notify.py b/cmdb-api/api/lib/notify.py index 75921a0f..606de76c 100644 --- a/cmdb-api/api/lib/notify.py +++ b/cmdb-api/api/lib/notify.py @@ -11,6 +11,8 @@ from api.lib.common_setting.notice_config import NoticeConfigCRUD from api.lib.mail import send_mail +NOTIFY_HTTP_TIMEOUT = 5 + def _request_messenger(subject, body, tos, sender, payload): params = dict(sender=sender, title=subject, @@ -49,7 +51,7 @@ def _request_messenger(subject, body, tos, sender, payload): if not url.endswith("message"): url = "{}/v1/message".format(url) - resp = requests.post(url, json=params) + resp = requests.post(url, json=params, timeout=NOTIFY_HTTP_TIMEOUT) if resp.status_code != 200: raise Exception(resp.text) diff --git a/cmdb-api/api/lib/perm/acl/acl.py b/cmdb-api/api/lib/perm/acl/acl.py index a8294017..5677d5f8 100644 --- a/cmdb-api/api/lib/perm/acl/acl.py +++ b/cmdb-api/api/lib/perm/acl/acl.py @@ -27,13 +27,15 @@ from api.models.acl import ResourceType from api.models.acl import Role +ACL_HTTP_TIMEOUT = 5 + def get_access_token(): url = "{0}/acl/apps/token".format(current_app.config.get('ACL_URI')) payload = dict(app_id=current_app.config.get('APP_ID'), - secret_key=hashlib.md5(current_app.config.get('APP_SECRET_KEY').encode('utf-8')).hexdigest()) + secret_key=hashlib.sha256(current_app.config.get('APP_SECRET_KEY').encode('utf-8')).hexdigest()) try: - res = requests.post(url, data=payload).json() + res = requests.post(url, data=payload, timeout=ACL_HTTP_TIMEOUT).json() return res.get("token") except Exception as e: current_app.logger.error(str(e)) @@ -208,7 +210,8 @@ def authenticate_with_token(token): url = "{0}/acl/auth_with_token".format(current_app.config.get('ACL_URI')) try: return requests.post(url, json={"token": token}, - headers={'App-Access-Token': AccessTokenCache.get()}).json() + headers={'App-Access-Token': AccessTokenCache.get()}, + timeout=ACL_HTTP_TIMEOUT).json() except: return {} diff --git a/cmdb-api/api/lib/perm/acl/app.py b/cmdb-api/api/lib/perm/acl/app.py index cf364326..ac0fd830 100644 --- a/cmdb-api/api/lib/perm/acl/app.py +++ b/cmdb-api/api/lib/perm/acl/app.py @@ -2,6 +2,7 @@ import datetime import hashlib +import hmac import jwt from flask import abort @@ -79,7 +80,18 @@ def _get_by_key(key): @classmethod def gen_token(cls, key, secret): app = cls._get_by_key(key) or abort(404, ErrFormat.app_not_found.format("key={}".format(key))) - secret != hashlib.md5(app.secret_key.encode('utf-8')).hexdigest() and abort(403, ErrFormat.app_secret_invalid) + if not isinstance(secret, str): + abort(403, ErrFormat.app_secret_invalid) + secret_sha256 = hashlib.sha256(app.secret_key.encode('utf-8')).hexdigest() + authenticated = hmac.compare_digest(secret_sha256, secret) + + if not authenticated and current_app.config.get("ACL_ALLOW_LEGACY_MD5_APP_SECRET", False): + # Backward compatibility for old clients; disabled by default. + secret_md5 = hashlib.md5(app.secret_key.encode('utf-8')).hexdigest() # nosec B324 + authenticated = hmac.compare_digest(secret_md5, secret) + + if not authenticated: + abort(403, ErrFormat.app_secret_invalid) token = jwt.encode({ 'sub': app.name, diff --git a/cmdb-api/api/lib/perm/authentication/oauth2/routing.py b/cmdb-api/api/lib/perm/authentication/oauth2/routing.py index dfc42d86..c691bf04 100644 --- a/cmdb-api/api/lib/perm/authentication/oauth2/routing.py +++ b/cmdb-api/api/lib/perm/authentication/oauth2/routing.py @@ -23,21 +23,35 @@ from api.lib.perm.acl.resp_format import ErrFormat blueprint = Blueprint('oauth2', __name__) +OAUTH_HTTP_TIMEOUT = 5 + + +def _safe_next_path(target): + if not target: + return None + + parsed = urlparse(target) + if parsed.scheme or parsed.netloc or target.startswith('//') or not target.startswith('/'): + return None + + return target @blueprint.route('/api//login') def login(auth_type): config = AuthenticateDataCRUD(auth_type.upper()).get() - if request.values.get("next"): - session["next"] = request.values.get("next") + next_path = _safe_next_path(request.values.get("next")) + if next_path: + session["next"] = next_path session[f'{auth_type}_state'] = secrets.token_urlsafe(16) auth_type = auth_type.upper() - redirect_uri = "{}://{}{}".format(urlparse(request.referrer).scheme, - urlparse(request.referrer).netloc, + referrer = request.referrer or request.host_url + redirect_uri = "{}://{}{}".format(urlparse(referrer).scheme, + urlparse(referrer).netloc, url_for('oauth2.callback', auth_type=auth_type.lower())) qs = urlencode({ 'client_id': config['client_id'], @@ -55,9 +69,9 @@ def callback(auth_type): auth_type = auth_type.upper() config = AuthenticateDataCRUD(auth_type).get() - redirect_url = session.get("next") or config.get('after_login') or '/' + redirect_url = _safe_next_path(session.get("next")) or config.get('after_login') or '/' - if request.values['state'] != session.get(f'{auth_type.lower()}_state'): + if request.values.get('state') != session.get(f'{auth_type.lower()}_state'): return abort(401, "state is invalid") if 'code' not in request.values: @@ -69,7 +83,7 @@ def callback(auth_type): 'code': request.values['code'], 'grant_type': current_app.config[f'{auth_type}_GRANT_TYPE'], 'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True), - }, headers={'Accept': 'application/json'}) + }, headers={'Accept': 'application/json'}, timeout=OAUTH_HTTP_TIMEOUT) if response.status_code != 200: current_app.logger.error(response.text) return abort(401) @@ -80,7 +94,7 @@ def callback(auth_type): response = requests.get(config['user_info']['url'], headers={ 'Authorization': 'Bearer {}'.format(access_token), 'Accept': 'application/json', - }) + }, timeout=OAUTH_HTTP_TIMEOUT) if response.status_code != 200: return abort(401) diff --git a/cmdb-api/api/lib/secrets/inner.py b/cmdb-api/api/lib/secrets/inner.py index 13025900..ed951091 100644 --- a/cmdb-api/api/lib/secrets/inner.py +++ b/cmdb-api/api/lib/secrets/inner.py @@ -369,7 +369,13 @@ def print_token(cls, shares, root_token): "unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL) print() - print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL) + show_root_token = str(os.environ.get("CMDB_SHOW_ROOT_TOKEN_ON_INIT", "")).lower() == "true" + if show_root_token: + print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL) + else: + token = root_token.decode("utf-8") + masked = "{0}...{1}".format(token[:4], token[-4:]) if len(token) > 8 else "********" + print(Fore.GREEN + "root token (masked): " + masked + Style.RESET_ALL) @classmethod def print_response(cls, data): diff --git a/cmdb-api/api/lib/secrets/vault.py b/cmdb-api/api/lib/secrets/vault.py index a5746f55..d2ed2cd3 100644 --- a/cmdb-api/api/lib/secrets/vault.py +++ b/cmdb-api/api/lib/secrets/vault.py @@ -2,6 +2,7 @@ from base64 import b64encode import hvac +import os class VaultClient: @@ -128,7 +129,9 @@ def decode_base64(cls, encoded_string): if __name__ == "__main__": _base_url = "http://localhost:8200" - _token = "your token" + _token = os.environ.get("VAULT_TOKEN", "") + if not _token: + raise RuntimeError("VAULT_TOKEN is required") _path = "test001" # Example diff --git a/cmdb-api/api/lib/utils.py b/cmdb-api/api/lib/utils.py index f1b397c1..2211ff42 100644 --- a/cmdb-api/api/lib/utils.py +++ b/cmdb-api/api/lib/utils.py @@ -5,7 +5,7 @@ import elasticsearch import redis import six -from Crypto.Cipher import AES +from Cryptodome.Cipher import AES from elasticsearch import Elasticsearch from flask import current_app diff --git a/cmdb-api/api/lib/webhook.py b/cmdb-api/api/lib/webhook.py index b8e36038..81db8ef8 100644 --- a/cmdb-api/api/lib/webhook.py +++ b/cmdb-api/api/lib/webhook.py @@ -8,6 +8,9 @@ from requests.auth import HTTPBasicAuth from requests_oauthlib import OAuth2Session +WEBHOOK_DEFAULT_TIMEOUT = 5 +WEBHOOK_MAX_TIMEOUT = 30 + class BearerAuth(requests.auth.AuthBase): def __init__(self, token): @@ -94,6 +97,14 @@ def webhook_request(webhook, payload): data = Template(json.dumps(webhook.get('body', ''))).render(payload).encode('utf-8') auth = _wrap_auth(**webhook.get('authorization', {})) + timeout = webhook.get('timeout', WEBHOOK_DEFAULT_TIMEOUT) + try: + timeout = float(timeout) + if timeout <= 0: + timeout = WEBHOOK_DEFAULT_TIMEOUT + except (TypeError, ValueError): + timeout = WEBHOOK_DEFAULT_TIMEOUT + timeout = min(timeout, WEBHOOK_MAX_TIMEOUT) if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0': request = getattr(auth, webhook.get('method', 'GET').lower()) @@ -105,5 +116,6 @@ def webhook_request(webhook, payload): params=params, headers=headers or None, data=data, - auth=auth + auth=auth, + timeout=timeout, ) diff --git a/cmdb-api/api/models/acl.py b/cmdb-api/api/models/acl.py index 61162b0e..1a1362c2 100644 --- a/cmdb-api/api/models/acl.py +++ b/cmdb-api/api/models/acl.py @@ -3,12 +3,14 @@ import copy import hashlib +import hmac from datetime import datetime from flask import current_app from flask import session from flask_sqlalchemy import BaseQuery +from api.extensions import bcrypt from api.extensions import db from api.lib.database import CRUDModel from api.lib.database import Model @@ -19,6 +21,42 @@ from api.lib.perm.acl.resp_format import ErrFormat +def _build_signatures(path, secret, args): + values = "".join([str(i) for i in (args or [])]) + payload = '{0}{1}{2}'.format(path or "", secret or "", values).encode("utf-8") + + signatures = { + "sha256": hashlib.sha256(payload).hexdigest(), + } + if current_app.config.get("ACL_ALLOW_LEGACY_SHA1_SIGNATURE", False): + # Backward compatibility for old clients; disabled by default. + signatures["sha1"] = hashlib.sha1(payload).hexdigest() # nosec B324 + + return signatures + + +def _verify_signature(path, secret, args, provided): + if isinstance(provided, bytes): + provided = provided.decode("utf-8", "ignore") + if not isinstance(provided, str): + return False + + signatures = _build_signatures(path, secret, args) + if hmac.compare_digest(signatures["sha256"], provided): + return True + + return "sha1" in signatures and hmac.compare_digest(signatures["sha1"], provided) + + +def _is_bcrypt_hash(password): + return isinstance(password, str) and password.startswith(("$2a$", "$2b$", "$2y$")) + + +def _upgrade_legacy_password(principal, password): + principal.password = password + return True + + class App(Model): __tablename__ = "acl_apps" @@ -55,8 +93,7 @@ def authenticate_with_key(self, key, secret, args, path): user = self.filter(User.key == key).filter(User.deleted.is_(False)).filter(User.block == 0).first() if not user: return None, False - if user and hashlib.sha1('{0}{1}{2}'.format( - path, user.secret, "".join(args)).encode("utf-8")).hexdigest() == secret: + if user and _verify_signature(path, user.secret, args, secret): authenticated = True else: authenticated = False @@ -132,14 +169,30 @@ def _get_password(self): return self._password def _set_password(self, password): - self._password = hashlib.md5(password.encode('utf-8')).hexdigest() + if password: + self._password = bcrypt.generate_password_hash(password).decode('utf-8') password = db.synonym("_password", descriptor=property(_get_password, _set_password)) def check_password(self, password): if self.password is None: return False - return self.password == password or self.password == hashlib.md5(password.encode('utf-8')).hexdigest() + if _is_bcrypt_hash(self.password): + try: + return bcrypt.check_password_hash(self.password, password) + except ValueError: + return False + + if current_app.config.get("ACL_ALLOW_LEGACY_PLAINTEXT_PASSWORD", False): + if hmac.compare_digest(self.password, password): + return _upgrade_legacy_password(self, password) + + if current_app.config.get("ACL_ALLOW_LEGACY_MD5_PASSWORD", False): + legacy_md5 = hashlib.md5(password.encode('utf-8')).hexdigest() # nosec B324 + if hmac.compare_digest(self.password, legacy_md5): + return _upgrade_legacy_password(self, password) + + return False class RoleQuery(BaseQuery): @@ -162,8 +215,7 @@ def authenticate_with_key(self, key, secret, args, path): role = self.filter(Role.key == key).filter(Role.deleted.is_(False)).first() if not role: return None, False - if role and hashlib.sha1('{0}{1}{2}'.format( - path, role.secret, "".join(args)).encode("utf-8")).hexdigest() == secret: + if role and _verify_signature(path, role.secret, args, secret): authenticated = True else: authenticated = False @@ -188,14 +240,29 @@ def _get_password(self): def _set_password(self, password): if password: - self._password = hashlib.md5(password.encode('utf-8')).hexdigest() + self._password = bcrypt.generate_password_hash(password).decode('utf-8') password = db.synonym("_password", descriptor=property(_get_password, _set_password)) def check_password(self, password): if self.password is None: return False - return self.password == password or self.password == hashlib.md5(password.encode('utf-8')).hexdigest() + if _is_bcrypt_hash(self.password): + try: + return bcrypt.check_password_hash(self.password, password) + except ValueError: + return False + + if current_app.config.get("ACL_ALLOW_LEGACY_PLAINTEXT_PASSWORD", False): + if hmac.compare_digest(self.password, password): + return _upgrade_legacy_password(self, password) + + if current_app.config.get("ACL_ALLOW_LEGACY_MD5_PASSWORD", False): + legacy_md5 = hashlib.md5(password.encode('utf-8')).hexdigest() # nosec B324 + if hmac.compare_digest(self.password, legacy_md5): + return _upgrade_legacy_password(self, password) + + return False class RoleRelation(Model): diff --git a/cmdb-api/api/views/acl/user.py b/cmdb-api/api/views/acl/user.py index 51bcca56..0806eb3a 100644 --- a/cmdb-api/api/views/acl/user.py +++ b/cmdb-api/api/views/acl/user.py @@ -23,6 +23,8 @@ from api.lib.utils import get_page_size from api.resource import APIView +HR_HTTP_TIMEOUT = 5 + class GetUserInfoView(APIView): url_prefix = "/users/info" @@ -136,7 +138,7 @@ class UserOnTheJobView(APIView): def get(self): if current_app.config.get('HR_URI'): try: - return self.jsonify(requests.get(current_app.config["HR_URI"]).json()) + return self.jsonify(requests.get(current_app.config["HR_URI"], timeout=HR_HTTP_TIMEOUT).json()) except: return abort(400, ErrFormat.invalid_request) else: diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index 5f256c1d..c10a6e75 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -12,16 +12,16 @@ Flask==2.2.5 Flask-Bcrypt==1.0.1 flask-babel==4.0.0 Flask-Caching==2.0.2 -Flask-Cors==4.0.0 +Flask-Cors==4.0.2 Flask-Login>=0.6.2 Flask-Migrate==2.5.2 Flask-RESTful==0.3.10 Flask-SQLAlchemy==3.0.5 future==0.18.3 -gunicorn==21.0.1 +gunicorn==22.0.0 hvac==2.0.0 itsdangerous==2.1.2 -Jinja2==3.1.2 +Jinja2==3.1.6 jinja2schema==0.1.4 jsonschema==4.18.0 kombu>=5.3.1 @@ -34,21 +34,21 @@ Pillow>=10.0.1 pycryptodome==3.12.0 cryptography>=41.0.2 PyJWT==2.4.0 -PyMySQL==1.1.0 +PyMySQL==1.1.1 ldap3==2.9.1 PyYAML==6.0.1 redis==4.6.0 python-redis-lock==4.0.0 -requests==2.31.0 +requests==2.32.4 requests_oauthlib==1.3.1 -markdownify==0.11.6 +markdownify==0.14.1 six==1.16.0 SQLAlchemy==1.4.49 supervisor==4.0.3 timeout-decorator==0.5.0 toposort==1.10 treelib==1.6.1 -Werkzeug==2.2.3 +Werkzeug==2.3.8 WTForms==3.0.0 shamir~=17.12.0 pycryptodomex>=3.19.0 diff --git a/cmdb-api/settings.example.py b/cmdb-api/settings.example.py index d80531d1..91d52f85 100644 --- a/cmdb-api/settings.example.py +++ b/cmdb-api/settings.example.py @@ -142,6 +142,11 @@ # # permission WHITE_LIST = ['127.0.0.1'] USE_ACL = True +# Legacy compatibility switches. Keep disabled in new deployments. +ACL_ALLOW_LEGACY_MD5_APP_SECRET = False +ACL_ALLOW_LEGACY_SHA1_SIGNATURE = False +ACL_ALLOW_LEGACY_MD5_PASSWORD = False +ACL_ALLOW_LEGACY_PLAINTEXT_PASSWORD = False # # elastic search ES_HOST = '127.0.0.1' diff --git a/cmdb-ui/src/directive/highlight/highlight.js b/cmdb-ui/src/directive/highlight/highlight.js index d76688a3..e0c017ee 100644 --- a/cmdb-ui/src/directive/highlight/highlight.js +++ b/cmdb-ui/src/directive/highlight/highlight.js @@ -1,14 +1,34 @@ import './highlight.less' +const escapeRegExp = (value) => { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +const escapeHtml = (value) => { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +const sanitizeClassName = (value) => { + const className = value ? `${value}` : 'ops-text-highlight' + return /^[A-Za-z0-9_-]+$/.test(className) ? className : 'ops-text-highlight' +} + const highlight = (el, binding) => { - if (binding.value.value) { - let testValue = `${binding.value.value}` - if (['(', ')', '$'].includes(testValue)) { - testValue = `\\${testValue}` - } - const regex = new RegExp(`(${testValue})`, 'gi') - el.innerHTML = el.innerText.replace(regex, `$1`) - } + const options = (binding && binding.value) || {} + if (options.value === undefined || options.value === null || `${options.value}` === '') { + return + } + + const text = escapeHtml(el.innerText || '') + const keyword = escapeRegExp(`${options.value}`) + const className = sanitizeClassName(options.class) + const regex = new RegExp(`(${keyword})`, 'gi') + el.innerHTML = text.replace(regex, `$1`) } export default highlight diff --git a/cmdb-ui/src/modules/cmdb/3rd/relation-graph/core4vue/SeeksRGNode.vue b/cmdb-ui/src/modules/cmdb/3rd/relation-graph/core4vue/SeeksRGNode.vue index 2ade7647..5f7427e3 100644 --- a/cmdb-ui/src/modules/cmdb/3rd/relation-graph/core4vue/SeeksRGNode.vue +++ b/cmdb-ui/src/modules/cmdb/3rd/relation-graph/core4vue/SeeksRGNode.vue @@ -13,7 +13,7 @@ -
+
- +
-
+
@@ -220,6 +220,14 @@ export default { this.onNodeClick(this.nodeProps, e) } }, + safeHtml(content) { + if (this.graphSetting && this.graphSetting.allowUnsafeHtml === true) { + return content || '' + } + const div = document.createElement('div') + div.textContent = content || '' + return div.innerHTML + }, // beforeEnter(el) { // console.log('beforeEnter') // el.style.opacity = 0 diff --git a/cmdb-ui/src/modules/cmdb/views/ci/modules/MetadataDrawer.vue b/cmdb-ui/src/modules/cmdb/views/ci/modules/MetadataDrawer.vue index 709555e7..217de09b 100644 --- a/cmdb-ui/src/modules/cmdb/views/ci/modules/MetadataDrawer.vue +++ b/cmdb-ui/src/modules/cmdb/views/ci/modules/MetadataDrawer.vue @@ -56,14 +56,13 @@ { label: $t('no'), value: false }, ] " - type="html" > @@ -196,9 +195,8 @@ export default { .trim() .toLowerCase() if (filterName) { - const filterRE = new RegExp(filterName, 'gi') const searchProps = ['name', 'alias', 'value_type'] - const rest = this.tableData.filter((item) => + this.list = this.tableData.filter((item) => searchProps.some( (key) => XEUtils.toValueString(item[key]) @@ -206,16 +204,6 @@ export default { .indexOf(filterName) > -1 ) ) - this.list = rest.map((row) => { - const item = Object.assign({}, row) - searchProps.forEach((key) => { - item[key] = XEUtils.toValueString(item[key]).replace( - filterRE, - (match) => `${match}` - ) - }) - return item - }) } else { this.list = this.tableData } diff --git a/cmdb-ui/src/modules/cmdb/views/resource_search_2/relationSearch/components/ciTable.vue b/cmdb-ui/src/modules/cmdb/views/resource_search_2/relationSearch/components/ciTable.vue index 845ff6c5..4929f1f5 100644 --- a/cmdb-ui/src/modules/cmdb/views/resource_search_2/relationSearch/components/ciTable.vue +++ b/cmdb-ui/src/modules/cmdb/views/resource_search_2/relationSearch/components/ciTable.vue @@ -210,12 +210,34 @@ export default { return {} }, methods: { + escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + }, + escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + }, markSearchValue(text) { - if (!text || !this.searchValue) { - return text + if (text === undefined || text === null) { + return '' + } + + const safeText = this.escapeHtml(text) + if (!this.searchValue) { + return safeText } - const regex = new RegExp(`(${this.searchValue})`, 'gi') - return String(text).replace( + + const keyword = this.escapeRegExp(this.searchValue) + if (!keyword) { + return safeText + } + + const regex = new RegExp(`(${keyword})`, 'gi') + return safeText.replace( regex, `$1` ) diff --git a/cmdb-ui/src/modules/cmdb/views/resource_search_2/resourceSearch/components/attrDisplay.vue b/cmdb-ui/src/modules/cmdb/views/resource_search_2/resourceSearch/components/attrDisplay.vue index 25bf6798..c13fecd1 100644 --- a/cmdb-ui/src/modules/cmdb/views/resource_search_2/resourceSearch/components/attrDisplay.vue +++ b/cmdb-ui/src/modules/cmdb/views/resource_search_2/resourceSearch/components/attrDisplay.vue @@ -42,8 +42,8 @@ }" > /g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + }, + escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + }, markSearchValue(text) { - if (!text || !this.searchValue) { - return text + if (text === undefined || text === null) { + return '' + } + + const safeText = this.escapeHtml(text) + if (!this.searchValue) { + return safeText } - const regex = new RegExp(`(${this.searchValue})`, 'gi') - return String(text).replace( + + const keyword = this.escapeRegExp(this.searchValue) + if (!keyword) { + return safeText + } + + const regex = new RegExp(`(${keyword})`, 'gi') + return safeText.replace( regex, `$1` ) diff --git a/cmdb-ui/src/utils/request.js b/cmdb-ui/src/utils/request.js index f4533595..53604dec 100644 --- a/cmdb-ui/src/utils/request.js +++ b/cmdb-ui/src/utils/request.js @@ -20,10 +20,13 @@ const service = axios.create({ const err = (error) => { console.log(error) const reg = /5\d{2}/g - if (error.response && reg.test(error.response.status)) { - const errorMsg = ((error.response || {}).data || {}).message || i18n.t('requestServiceError') + const response = (error && error.response) || {} + const config = (error && error.config) || {} + const status = response.status + if (status && reg.test(String(status))) { + const errorMsg = (response.data || {}).message || i18n.t('requestServiceError') message.error(errorMsg) - } else if (error.response.status === 412) { + } else if (status === 412) { let seconds = 5 notification.warning({ key: 'notification', @@ -49,14 +52,15 @@ const err = (error) => { duration: seconds }) }, 1000) - } else if (error.config.url === '/api/v0.1/ci_types/can_define_computed' || error.config.isShowMessage === false) { + } else if (config.url === '/api/v0.1/ci_types/can_define_computed' || config.isShowMessage === false) { } else { - const errorMsg = ((error.response || {}).data || {}).message || i18n.t('requestError') + const errorMsg = (response.data || {}).message || i18n.t('requestError') message.error(`${errorMsg}`) } - if (error.response) { - console.log(error.config.url) - if (error.response.status === 401 && router.path === '/user/login') { + if (status) { + console.log(config.url) + const currentPath = (router.currentRoute || {}).path || router.path + if (status === 401 && currentPath === '/user/login') { window.location.href = '/user/logout' } } diff --git a/cmdb-ui/src/views/noticeCenter/index.vue b/cmdb-ui/src/views/noticeCenter/index.vue index 28c15279..a7b211e7 100644 --- a/cmdb-ui/src/views/noticeCenter/index.vue +++ b/cmdb-ui/src/views/noticeCenter/index.vue @@ -85,7 +85,7 @@