Compare commits

...

56 Commits
2.3.7 ... 2.3.9

Author SHA1 Message Date
pycook
100a889cb8 fix(api): CI revoke permission (#337) 2023-12-25 12:15:20 +08:00
kdyq007
b093569453 [更新] 修复 LDAP 登录失败的问题 (#336)
Co-authored-by: sherlock <sherlock@gmail.com>
2023-12-25 09:36:31 +08:00
pycook
3919dfdfbb release: 2.3.9 2023-12-23 12:51:09 +08:00
pycook
ef85ba2542 feat(api): update cmdb-init-acl commands (#335) 2023-12-23 12:44:01 +08:00
pycook
3d5c2ec5bc Merge branch 'master' of github.com:veops/cmdb 2023-12-23 12:31:52 +08:00
pycook
c143d6ae5b fix(api): role grant 2023-12-23 12:30:52 +08:00
simontigers
10b273ee81 feat(api): add update_last_login_by_uid (#333) 2023-12-22 18:43:20 +08:00
wang-liang0615
855cb91b31 feat(cmdb-ui):ci type import&export,pref(cmdb-ui):download ci xlsx name, pref(cmdb-ui):ci detail history merge row method (#331)
* pref(cmdb-ui):download ci xlsx name

* pref(cmdb-ui):ci detail history merge row method

* feat(cmdb-ui):ci type import&export
2023-12-22 15:42:20 +08:00
pycook
ffae57642c fix(api): ci relation search
fix(api): ci relation search
2023-12-22 15:35:02 +08:00
simontigers
9ed1108c20 feat(api): add get_file_binary_str and save (#329) 2023-12-22 15:33:05 +08:00
simontigers
72b2f8b6de fix(api): refresh rid after create and import employee (#328) 2023-12-22 15:24:48 +08:00
pycook
20f3e917fe pref(api): import and export of CIType templates
pref(api): import and export of CIType templates
2023-12-22 14:32:03 +08:00
pycook
c430515377 fix(api): add CI (#326) 2023-12-22 11:19:16 +08:00
simontigers
a6e2aca281 fix(api): svg upload (#321) 2023-12-21 18:58:35 +08:00
wang-liang0615
18313b7bd1 fix(acl_ui):permission (#325) 2023-12-21 17:22:49 +08:00
wang-liang0615
dbc44a8ad6 fix(ui):common double menu (#324) 2023-12-21 14:59:39 +08:00
wang-liang0615
f143e30cf5 feat(ui):批量导入模型根据create权限过滤&&模型配置页面权限 (#323) 2023-12-21 14:23:38 +08:00
wang-liang0615
4beece5a6e fix:open triggerForm from attributeCard (#322) 2023-12-21 14:18:00 +08:00
pycook
920295d955 feat: Fixed db volume name 2023-12-21 10:26:51 +08:00
pycook
6eb8ae1dac fix(api): CAS authentication 2023-12-20 12:10:00 +08:00
wang-liang0615
f1f86ce25a feat(ui):api_host annotation (#320) 2023-12-19 14:22:40 +08:00
pycook
090007487d fix(api): oauth2.0 authentication 2023-12-19 13:07:21 +08:00
wang-liang0615
aa98a304c1 feat:add auth common api_host (#319) 2023-12-19 11:23:27 +08:00
pycook
fe22e363b4 fix(api): ldap authentication 2023-12-19 00:16:56 +08:00
pycook
2d2fb6e1d6 release: v2.3.8 2023-12-18 20:08:19 +08:00
pycook
9894c77300 feat(ui): lint 2023-12-18 19:25:22 +08:00
simontigers
4ea947f741 fix: auth config (#318) 2023-12-18 18:27:06 +08:00
simontigers
e5ab2c2573 fix: auth config (#317) 2023-12-18 16:52:24 +08:00
wang-liang0615
5581fa8d0f lint(ui) (#316) 2023-12-18 16:41:21 +08:00
wang-liang0615
76c939fe5c fix(ui):401 redirect && feat(ui):add auth ldap test (#315) 2023-12-18 16:30:02 +08:00
wang-liang0615
6e9ce08e2c pref(cmdb-ui):change adt key & add adt alias (#314) 2023-12-18 16:07:52 +08:00
pycook
092a8b9b92 pref(api): A CIType allows repeated binding of auto-discovery rules (#313) 2023-12-16 17:56:14 +08:00
wang-liang0615
b45fd0cbbb fix:is_list edit bug (#312)
* feat(ui):auth setting

* fix:is_list edit bug
2023-12-15 13:19:55 +08:00
simontigers
5320ecfd62 fix(api): common_data (#311) 2023-12-15 10:56:04 +08:00
wang-liang0615
1d253d7ad3 feat(ui):auth setting (#310) 2023-12-15 10:33:38 +08:00
pycook
73d53f0440 pref(api): authentication and login log (#308)
* pref(api): authentication and login log

* feat(api): ldap and OAuth2.0
2023-12-14 19:53:08 +08:00
simontigers
d4a37af183 feat(api): auth config api (#309) 2023-12-14 19:39:21 +08:00
wang-liang0615
e03849b054 fix(cmdb-ui):batch upload cancel bug && download error (#306) 2023-12-13 14:50:53 +08:00
simontigers
faed3fe6a2 fix(api): get_employee_notice check data None (#305)
* fix(api): get_employee_notice check data None

* fix(api): remove path when save messager url
2023-12-13 09:45:42 +08:00
pycook
21c9d9accd feat(api): support OAuth2.0 and OIDC authentication, it has been tested with casdoor
feat(api): support OAuth2.0 and OIDC authentication, it has been tested with casdoor
2023-12-12 20:29:57 +08:00
wang-liang0615
a06599ce33 pref(cmdb-ui):batch upload for date type (#301) 2023-12-12 14:53:12 +08:00
pycook
2b69217136 fix(api): time data format
fix(api): time data format
2023-12-12 14:37:21 +08:00
wang-liang0615
03a3b8b169 Revert "pref(cmdb-ui):batch upload for date type (#298)" (#299)
This reverts commit cd319421d5.
2023-12-12 13:51:19 +08:00
wang-liang0615
cd319421d5 pref(cmdb-ui):batch upload for date type (#298)
* fix(cmdb-ui):set localstorage '' after unsubscribe ci

* pref(cmdb-ui):batch upload for date type
2023-12-12 13:38:08 +08:00
pycook
c918d54ea5 perf(api): ci delete (#297) 2023-12-12 11:09:32 +08:00
wang-liang0615
cf0ad7bad6 fix(cmdb-ui):set localstorage '' after unsubscribe ci (#296) 2023-12-12 09:38:50 +08:00
pycook
e0c8263542 feat(api): cas is compatible with casdoor
feat(api): cas is compatible with casdoor
2023-12-11 20:58:18 +08:00
pycook
275e8b15f3 Dev api 231211 (#294)
* fix(api): cas authentication

* feat(api): add lz4 package
2023-12-11 19:30:09 +08:00
simontigers
6ff942c107 feat(api): upload file save db (#292) 2023-12-11 18:22:33 +08:00
gmailnovo
d3c87ee500 feat: Handle '/dev/stdout' in Logger Configuration 2023-12-07 11:27:45 +08:00
simontigers
a4f65e7fc6 fix(api): Common create employee (#287)
* fix(api): add add_from arg in create employee

* fix(api): add check in acl call common after add user
2023-12-06 17:12:37 +08:00
wang-liang0615
10527bf9b8 pref(cmdb-ui):ci upload&delete concurrent 6 (#286) 2023-12-06 14:33:25 +08:00
pycook
0414121c27 docs: update local install 2023-11-30 16:18:33 +08:00
simontigers
edde467c87 fix(api): common_data delele (#282) 2023-11-30 13:03:09 +08:00
pycook
d525e1ec54 feat(api): only the role cmdb_admin can modify the CIType group (#280) 2023-11-29 17:40:12 +08:00
pycook
91c49b690f fix(api): get relation history 2023-11-28 20:37:36 +08:00
107 changed files with 5130 additions and 602 deletions

View File

@@ -62,6 +62,7 @@ alembic = "==1.7.7"
hvac = "==2.0.0"
colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2"
[dev-packages]
# Testing

View File

@@ -19,7 +19,8 @@ from flask.json.provider import DefaultJSONProvider
import api.views.entry
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets
from api.flask_cas import CAS
from api.lib.perm.authentication.cas import CAS
from api.lib.perm.authentication.oauth2 import OAuth2
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User
@@ -96,6 +97,7 @@ def create_app(config_object="settings"):
register_shell_context(app)
register_commands(app)
CAS(app)
OAuth2(app)
app.wsgi_app = ReverseProxy(app.wsgi_app)
configure_upload_dir(app)
@@ -192,10 +194,11 @@ def configure_logger(app):
app.logger.addHandler(handler)
log_file = app.config['LOG_PATH']
file_handler = RotatingFileHandler(log_file,
maxBytes=2 ** 30,
backupCount=7)
file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
if log_file and log_file != "/dev/stdout":
file_handler = RotatingFileHandler(log_file,
maxBytes=2 ** 30,
backupCount=7)
file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL']))

View File

@@ -117,7 +117,15 @@ def cmdb_init_acl():
# 1. add resource type
for resource_type in ResourceTypeEnum.all():
try:
ResourceTypeCRUD.add(app_id, resource_type, '', PermEnum.all())
perms = PermEnum.all()
if resource_type in (ResourceTypeEnum.CI_FILTER, ResourceTypeEnum.PAGE):
perms = [PermEnum.READ]
elif resource_type == ResourceTypeEnum.CI_TYPE_RELATION:
perms = [PermEnum.ADD, PermEnum.DELETE, PermEnum.GRANT]
elif resource_type == ResourceTypeEnum.RELATION_VIEW:
perms = [PermEnum.READ, PermEnum.UPDATE, PermEnum.DELETE, PermEnum.GRANT]
ResourceTypeCRUD.add(app_id, resource_type, '', perms)
except AbortException:
pass

View File

@@ -299,3 +299,20 @@ def common_check_new_columns():
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)
@click.command()
@with_appcontext
def common_sync_file_to_db():
from api.lib.common_setting.upload_file import CommonFileCRUD
CommonFileCRUD.sync_file_to_db()
@click.command()
@with_appcontext
@click.option('--value', type=click.INT, default=-1)
def set_auth_auto_redirect_enable(value):
if value < 0:
return
from api.lib.common_setting.common_data import CommonDataCRUD
CommonDataCRUD.set_auth_auto_redirect_enable(value)

View File

@@ -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"],

View File

@@ -3,11 +3,6 @@ import datetime
import json
import os
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
from api.extensions import db
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
from api.lib.cmdb.cache import CITypeAttributeCache
@@ -28,6 +23,10 @@ from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryRule
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
PWD = os.path.abspath(os.path.dirname(__file__))
@@ -251,20 +250,17 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e)
return abort(400, str(e))
def _can_add(self, **kwargs):
self.cls.get_by(type_id=kwargs['type_id'], adr_id=kwargs.get('adr_id') or None) and abort(
400, ErrFormat.ad_duplicate)
# self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
@staticmethod
def _can_add(**kwargs):
if kwargs.get('adr_id'):
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
if not adr.is_plugin:
other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
if other:
ci_type = CITypeCache.get(other.type_id)
return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
# if not adr.is_plugin:
# other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
# if other:
# ci_type = CITypeCache.get(other.type_id)
# return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)

View File

@@ -395,8 +395,9 @@ class CIManager(object):
k not in ci_type_attrs_alias and _no_attribute_policy == ExistPolicy.REJECT):
return abort(400, ErrFormat.attribute_not_found.format(k))
if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and (
ci_type_attrs_alias.get(k) not in limit_attrs):
_attr_name = ((ci_type_attrs_name.get(k) and ci_type_attrs_name[k].name) or
(ci_type_attrs_alias.get(k) and ci_type_attrs_alias[k].name))
if limit_attrs and _attr_name not in limit_attrs:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
@@ -514,9 +515,9 @@ class CIManager(object):
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
for attr_name in attr_names:
value_table = TableMap(attr_name=attr_name).table
attrs = [AttributeCache.get(attr.attr_id) for attr in attrs]
for attr in attrs:
value_table = TableMap(attr=attr).table
for item in value_table.get_by(ci_id=ci_id, to_dict=False):
item.delete(commit=False)

View File

@@ -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
@@ -22,6 +24,7 @@ from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.value import AttributeValueManager
@@ -75,12 +78,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 +117,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 +138,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 +254,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 +293,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])
@@ -576,6 +589,11 @@ class CITypeRelationManager(object):
ci_type_dict = CITypeCache.get(type_id).to_dict()
ci_type_dict["ctr_id"] = relation_inst.id
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
if attr_filter:
ci_type_dict["attributes"] = [attr for attr in (ci_type_dict["attributes"] or [])
if attr['name'] in attr_filter]
ci_type_dict["relation_type"] = relation_inst.relation_type.name
ci_type_dict["constraint"] = relation_inst.constraint
@@ -725,7 +743,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 +909,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 +969,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 +1235,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

View File

@@ -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):

View File

@@ -135,7 +135,7 @@ class AttributeHistoryManger(object):
from api.lib.cmdb.ci import CIManager
cis = CIManager().get_cis_by_ids(list(ci_ids),
unique_required=True)
cis = {i['_id']: i for i in cis}
cis = {i['_id']: i for i in cis if i}
return total, res, cis

View File

@@ -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):

View File

@@ -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 类型必须是列表"

View File

@@ -28,7 +28,8 @@ class Search(object):
count=None,
sort=None,
reverse=False,
ancestor_ids=None):
ancestor_ids=None,
has_m2m=None):
self.orig_query = query
self.fl = fl
self.facet_field = facet_field
@@ -45,14 +46,15 @@ class Search(object):
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.has_m2m = False
if self.ancestor_ids:
self.has_m2m = True
else:
level = level[0] if isinstance(level, list) and level else level
for _l, c in self.level2constraint.items():
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
self.has_m2m = has_m2m or False
if not self.has_m2m:
if self.ancestor_ids:
self.has_m2m = True
else:
level = level[0] if isinstance(level, list) and level else level
for _l, c in self.level2constraint.items():
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
def _get_ids(self, ids):
if self.level[-1] == 1 and len(ids) == 1:
@@ -90,12 +92,12 @@ class Search(object):
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or []))
ids = [j for i in _tmp for j in i]
if not key:
return []
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or []))
ids = [j for i in _tmp for j in i]
if level in self.level:
merge_ids.extend(ids)

View File

@@ -12,7 +12,7 @@ import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum
TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d')
def string2int(x):

View File

@@ -1,14 +1,24 @@
from flask import abort
import copy
import json
from flask import abort, current_app
from ldap3 import Connection
from ldap3 import Server
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
from ldap3 import AUTO_BIND_NO_TLS
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CommonData
from api.lib.utils import AESCrypto
from api.lib.common_setting.const import AuthCommonConfig, AuthenticateType, AuthCommonConfigAutoRedirect, TestType
class CommonDataCRUD(object):
@staticmethod
def get_data_by_type(data_type):
CommonDataCRUD.check_auth_type(data_type)
return CommonData.get_by(data_type=data_type)
@staticmethod
@@ -18,6 +28,8 @@ class CommonDataCRUD(object):
@staticmethod
def create_new_data(data_type, **kwargs):
try:
CommonDataCRUD.check_auth_type(data_type)
return CommonData.create(data_type=data_type, **kwargs)
except Exception as e:
db.session.rollback()
@@ -29,6 +41,7 @@ class CommonDataCRUD(object):
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
CommonDataCRUD.check_auth_type(existed.data_type)
return existed.update(**kwargs)
except Exception as e:
db.session.rollback()
@@ -40,7 +53,230 @@ class CommonDataCRUD(object):
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
CommonDataCRUD.check_auth_type(existed.data_type)
existed.soft_delete()
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def check_auth_type(data_type):
if data_type in list(AuthenticateType.all()) + [AuthCommonConfig]:
abort(400, ErrFormat.common_data_not_support_auth_type.format(data_type))
@staticmethod
def set_auth_auto_redirect_enable(_value: int):
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig, to_dict=False)
if not existed:
CommonDataCRUD.create_new_data(AuthCommonConfig, data={AuthCommonConfigAutoRedirect: _value})
else:
data = existed.data
data = copy.deepcopy(existed.data) if data else {}
data[AuthCommonConfigAutoRedirect] = _value
CommonDataCRUD.update_data(existed.id, data=data)
return True
@staticmethod
def get_auth_auto_redirect_enable():
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig)
if not existed:
return 0
data = existed.get('data', {})
if not data:
return 0
return data.get(AuthCommonConfigAutoRedirect, 0)
class AuthenticateDataCRUD(object):
common_type_list = [AuthCommonConfig]
def __init__(self, _type):
self._type = _type
self.record = None
self.decrypt_data = {}
def get_support_type_list(self):
return list(AuthenticateType.all()) + self.common_type_list
def get(self):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data
def get_by_key(self, _key):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data.get(_key, None)
def get_record(self, to_dict=False) -> CommonData:
return CommonData.get_by(first=True, data_type=self._type, to_dict=to_dict)
def get_record_with_decrypt(self) -> dict:
record = CommonData.get_by(first=True, data_type=self._type, to_dict=True)
if not record:
return {}
data = self.get_decrypt_dict(record.get('data', ''))
record['data'] = data
return record
def get_decrypt_dict(self, data):
decrypt_str = self.decrypt(data)
try:
return json.loads(decrypt_str)
except Exception as e:
abort(400, str(e))
def get_decrypt_data(self) -> dict:
self.record = self.get_record()
if not self.record:
return self.get_from_config()
return self.get_decrypt_dict(self.record.data)
def get_from_config(self):
return current_app.config.get(self._type, {})
def check_by_type(self) -> None:
existed = self.get_record()
if existed:
abort(400, ErrFormat.common_data_already_existed.format(self._type))
def create(self, data) -> CommonData:
self.check_by_type()
encrypt = data.pop('encrypt', None)
if encrypt is False:
return CommonData.create(data_type=self._type, data=data)
encrypted_data = self.encrypt(data)
try:
return CommonData.create(data_type=self._type, data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update_by_record(self, record, data) -> CommonData:
encrypt = data.pop('encrypt', None)
if encrypt is False:
return record.update(data=data)
encrypted_data = self.encrypt(data)
try:
return record.update(data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update(self, _id, data) -> CommonData:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
return self.update_by_record(existed, data)
@staticmethod
def delete(_id) -> None:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
existed.soft_delete()
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def encrypt(data) -> str:
if type(data) is dict:
try:
data = json.dumps(data)
except Exception as e:
abort(400, str(e))
return AESCrypto().encrypt(data)
@staticmethod
def decrypt(data) -> str:
return AESCrypto().decrypt(data)
@staticmethod
def get_enable_list():
all_records = CommonData.query.filter(
CommonData.data_type.in_(AuthenticateType.all()),
CommonData.deleted == 0
).all()
enable_list = []
for auth_type in AuthenticateType.all():
record = list(filter(lambda x: x.data_type == auth_type, all_records))
if not record:
config = current_app.config.get(auth_type, None)
if not config:
continue
if config.get('enable', False):
enable_list.append(dict(
auth_type=auth_type,
))
continue
try:
decrypt_data = json.loads(AuthenticateDataCRUD.decrypt(record[0].data))
except Exception as e:
current_app.logger.error(e)
continue
if decrypt_data.get('enable', 0) == 1:
enable_list.append(dict(
auth_type=auth_type,
))
auth_auto_redirect = CommonDataCRUD.get_auth_auto_redirect_enable()
return dict(
enable_list=enable_list,
auth_auto_redirect=auth_auto_redirect,
)
def test(self, test_type, data):
type_lower = self._type.lower()
func_name = f'test_{type_lower}'
if hasattr(self, func_name):
try:
return getattr(self, f'test_{type_lower}')(test_type, data)
except Exception as e:
abort(400, str(e))
abort(400, ErrFormat.not_support_test.format(self._type))
@staticmethod
def test_ldap(test_type, data):
ldap_server = data.get('ldap_server')
ldap_user_dn = data.get('ldap_user_dn', '{}')
server = Server(ldap_server, connect_timeout=2)
if not server.check_availability():
raise Exception(ErrFormat.ldap_server_connect_not_available)
else:
if test_type == TestType.Connect:
return True
username = data.get('username', None)
if not username:
raise Exception(ErrFormat.ldap_test_username_required)
user = ldap_user_dn.format(username)
password = data.get('password', None)
try:
Connection(server, user=user, password=password, auto_bind=AUTO_BIND_NO_TLS)
except LDAPBindError:
ldap_domain = data.get('ldap_domain')
user_with_domain = f"{username}@{ldap_domain}"
try:
Connection(server, user=user_with_domain, password=password, auto_bind=AUTO_BIND_NO_TLS)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
except LDAPSocketOpenError:
raise Exception(ErrFormat.ldap_server_connect_timeout)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
return True

View File

@@ -1,4 +1,6 @@
# -*- coding:utf-8 -*-
from urllib.parse import urlparse
from api.extensions import cache
from api.models.common_setting import CompanyInfo
@@ -11,6 +13,7 @@ class CompanyInfoCRUD(object):
@staticmethod
def create(**kwargs):
CompanyInfoCRUD.check_data(**kwargs)
res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info)
return res
@@ -22,10 +25,26 @@ class CompanyInfoCRUD(object):
if not existed:
existed = CompanyInfoCRUD.create(**kwargs)
else:
CompanyInfoCRUD.check_data(**kwargs)
existed = existed.update(**kwargs)
CompanyInfoCache.refresh(existed.info)
return existed
@staticmethod
def check_data(**kwargs):
info = kwargs.get('info', {})
info['messenger'] = CompanyInfoCRUD.check_messenger(info.get('messenger', None))
kwargs['info'] = info
@staticmethod
def check_messenger(messenger):
if not messenger:
return messenger
parsed_url = urlparse(messenger)
return f"{parsed_url.scheme}://{parsed_url.netloc}"
class CompanyInfoCache(object):
key = 'CompanyInfoCache::'
@@ -41,4 +60,4 @@ class CompanyInfoCache(object):
@classmethod
def refresh(cls, info):
cache.set(cls.key, info)
cache.set(cls.key, info)

View File

@@ -19,3 +19,19 @@ BotNameMap = {
'feishuApp': 'feishuBot',
'dingdingApp': 'dingdingBot',
}
class AuthenticateType(BaseEnum):
CAS = 'CAS'
OAUTH2 = 'OAUTH2'
OIDC = 'OIDC'
LDAP = 'LDAP'
AuthCommonConfig = 'AuthCommonConfig'
AuthCommonConfigAutoRedirect = 'auto_redirect'
class TestType(BaseEnum):
Connect = 'connect'
Login = 'login'

View File

@@ -15,10 +15,13 @@ from wtforms import validators
from api.extensions import db
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import COMMON_SETTING_QUEUE, OperatorType
from api.lib.common_setting.const import OperatorType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department
from api.tasks.common_setting import refresh_employee_acl_info, edit_employee_department_in_acl
acl_user_columns = [
'email',
'mobile',
@@ -137,7 +140,9 @@ class EmployeeCRUD(object):
@staticmethod
def add(**kwargs):
try:
return CreateEmployee().create_single(**kwargs)
res = CreateEmployee().create_single(**kwargs)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
return res
except Exception as e:
abort(400, str(e))
@@ -164,10 +169,9 @@ class EmployeeCRUD(object):
existed.update(**kwargs)
if len(e_list) > 0:
from api.tasks.common_setting import edit_employee_department_in_acl
edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid),
queue=COMMON_SETTING_QUEUE
queue=CMDB_QUEUE
)
return existed
@@ -291,7 +295,7 @@ class EmployeeCRUD(object):
employees = []
for r in pagination.items:
d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name
d['department_name'] = r.Department.department_name if r.Department else ''
employees.append(d)
return {
@@ -437,7 +441,7 @@ class EmployeeCRUD(object):
employees = []
for r in pagination.items:
d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name
d['department_name'] = r.Department.department_name if r.Department else ''
employees.append(d)
return {
@@ -563,6 +567,7 @@ class EmployeeCRUD(object):
for column in direct_columns:
tmp[column] = d.get(column, '')
notice_info = d.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
tmp.update(**notice_info)
results.append(tmp)
return results
@@ -570,6 +575,7 @@ class EmployeeCRUD(object):
@staticmethod
def import_employee(employee_list):
res = CreateEmployee().batch_create(employee_list)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
return res
@staticmethod
@@ -686,6 +692,27 @@ class EmployeeCRUD(object):
else:
abort(400, ErrFormat.column_name_not_support)
@staticmethod
def update_last_login_by_uid(uid, last_login=None):
employee = Employee.get_by(acl_uid=uid, first=True, to_dict=False)
if not employee:
return
if last_login:
try:
last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S')
except Exception as e:
last_login = datetime.now()
else:
last_login = datetime.now()
try:
employee.update(
last_login=last_login
)
return last_login
except Exception as e:
return
def get_user_map(key='uid', acl=None):
"""
@@ -726,6 +753,7 @@ class CreateEmployee(object):
try:
existed = self.check_acl_user(user_data)
if not existed:
user_data['add_from'] = 'common'
return self.acl.create_user(user_data)
return existed
except Exception as e:

View File

@@ -8,6 +8,9 @@ class ErrFormat(CommonErrFormat):
no_file_part = "没有文件部分"
file_is_required = "文件是必须的"
file_not_found = "文件不存在"
file_type_not_allowed = "文件类型不允许"
upload_failed = "上传失败: {}"
direct_supervisor_is_not_self = "直属上级不能是自己"
parent_department_is_not_self = "上级部门不能是自己"
@@ -56,6 +59,7 @@ class ErrFormat(CommonErrFormat):
email_send_timeout = "邮件发送超时"
common_data_not_found = "ID {} 找不到记录"
common_data_already_existed = "{} 已存在"
notice_platform_existed = "{} 已存在"
notice_not_existed = "{} 配置项不存在"
notice_please_config_messenger_first = "请先配置 messenger"
@@ -63,3 +67,11 @@ class ErrFormat(CommonErrFormat):
notice_bind_failed = "绑定失败: {}"
notice_bind_success = "绑定成功"
notice_remove_bind_success = "解绑成功"
not_support_test = "不支持的测试类型: {}"
not_support_auth_type = "不支持的认证类型: {}"
ldap_server_connect_timeout = "LDAP服务器连接超时"
ldap_server_connect_not_available = "LDAP服务器连接不可用"
ldap_test_unknown_error = "LDAP测试未知错误: {}"
common_data_not_support_auth_type = "通用数据不支持auth类型: {}"
ldap_test_username_required = "LDAP测试用户名必填"

View File

@@ -1,6 +1,14 @@
import base64
import uuid
import os
from io import BytesIO
from flask import abort, current_app
import lz4.frame
from api.lib.common_setting.utils import get_cur_time_str
from api.models.common_setting import CommonFile
from api.lib.common_setting.resp_format import ErrFormat
def allowed_file(filename, allowed_extensions):
@@ -14,3 +22,73 @@ def generate_new_file_name(name):
cur_str = get_cur_time_str('_')
return f"{prev_name}_{cur_str}_{uid}.{ext}"
class CommonFileCRUD:
@staticmethod
def add_file(**kwargs):
return CommonFile.create(**kwargs)
@staticmethod
def get_file(file_name, to_str=False):
existed = CommonFile.get_by(file_name=file_name, first=True, to_dict=False)
if not existed:
abort(400, ErrFormat.file_not_found)
uncompressed_data = lz4.frame.decompress(existed.binary)
return base64.b64encode(uncompressed_data).decode('utf-8') if to_str else BytesIO(uncompressed_data)
@staticmethod
def sync_file_to_db():
for p in ['UPLOAD_DIRECTORY_FULL']:
upload_path = current_app.config.get(p, None)
if not upload_path:
continue
for root, dirs, files in os.walk(upload_path):
for file in files:
file_path = os.path.join(root, file)
if not os.path.isfile(file_path):
continue
existed = CommonFile.get_by(file_name=file, first=True, to_dict=False)
if existed:
continue
with open(file_path, 'rb') as f:
data = f.read()
compressed_data = lz4.frame.compress(data)
try:
CommonFileCRUD.add_file(
origin_name=file,
file_name=file,
binary=compressed_data
)
current_app.logger.info(f'sync file {file} to db')
except Exception as e:
current_app.logger.error(f'sync file {file} to db error: {e}')
def get_file_binary_str(self, file_name):
return self.get_file(file_name, True)
def save_str_to_file(self, file_name, str_data):
try:
self.get_file(file_name)
current_app.logger.info(f'file {file_name} already exists')
return
except Exception as e:
# file not found
pass
bytes_data = base64.b64decode(str_data)
compressed_data = lz4.frame.compress(bytes_data)
try:
self.add_file(
origin_name=file_name,
file_name=file_name,
binary=compressed_data
)
current_app.logger.info(f'save_str_to_file {file_name} success')
except Exception as e:
current_app.logger.error(f"save_str_to_file error: {e}")

View File

@@ -94,7 +94,7 @@ class CRUDMixin(FormatMixin):
if any((isinstance(_id, six.string_types) and _id.isdigit(),
isinstance(_id, (six.integer_types, float))), ):
obj = getattr(cls, "query").get(int(_id))
if obj and not obj.deleted:
if obj and not getattr(obj, 'deleted', False):
return obj
@classmethod

View File

@@ -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,20 +138,20 @@ 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 ResourceCRUD.delete(resource.id)
def has_permission(self, resource_name, resource_type, perm, resource_id=None):
if is_app_admin(self.app_id):

View File

@@ -1,14 +1,19 @@
# -*- coding:utf-8 -*-
import datetime
import itertools
import json
from enum import Enum
from typing import List
from flask import has_request_context, request
from flask import has_request_context
from flask import request
from flask_login import current_user
from sqlalchemy import func
from api.extensions import db
from api.lib.perm.acl import AppCache
from api.models.acl import AuditLoginLog
from api.models.acl import AuditPermissionLog
from api.models.acl import AuditResourceLog
from api.models.acl import AuditRoleLog
@@ -283,6 +288,27 @@ class AuditCRUD(object):
return data
@staticmethod
def search_login(_, q=None, page=1, page_size=10, start=None, end=None):
query = db.session.query(AuditLoginLog)
if start:
query = query.filter(AuditLoginLog.login_at >= start)
if end:
query = query.filter(AuditLoginLog.login_at <= end)
if q:
query = query.filter(AuditLoginLog.username == q)
records = query.order_by(
AuditLoginLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
}
return data
@classmethod
def add_role_log(cls, app_id, operate_type: AuditOperateType,
scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict,
@@ -348,3 +374,30 @@ class AuditCRUD(object):
AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id,
operate_type=operate_type.value,
origin=origin, current=current, extra=extra, source=source.value)
@classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
if _id is not None:
existed = AuditLoginLog.get_by_id(_id)
if existed is not None:
existed.update(logout_at=logout_at)
return
payload = dict(username=username,
is_ok=is_ok,
description=description,
logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr,
browser=request.headers.get('User-Agent'),
)
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

View File

@@ -328,6 +328,8 @@ class ResourceCRUD(object):
AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete,
AuditScope.resource, resource.id, origin, {}, {})
return rebuilds
@classmethod
def delete_by_name(cls, name, type_id, app_id):
resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort(

View File

@@ -4,6 +4,9 @@ from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat):
login_succeed = "登录成功"
ldap_connection_failed = "连接LDAP服务失败"
invalid_password = "密码验证失败"
auth_only_with_app_token_failed = "应用 Token验证失败"
session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
@@ -17,11 +20,11 @@ class ErrFormat(CommonErrFormat):
role_exists = "角色 {} 已经存在!"
global_role_not_found = "全局角色 {} 不存在!"
global_role_exists = "全局角色 {} 已经存在!"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
resource_no_permission = "您没有资源: {}{} 权限"
admin_required = "需要管理员权限"
role_required = "需要角色: {}"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
app_is_ready_existed = "应用 {} 已经存在"
app_not_found = "应用 {} 不存在!"

View File

@@ -41,6 +41,7 @@ class UserCRUD(object):
@classmethod
def add(cls, **kwargs):
add_from = kwargs.pop('add_from', None)
existed = User.get_by(username=kwargs['username'])
existed and abort(400, ErrFormat.user_exists.format(kwargs['username']))
@@ -62,10 +63,12 @@ class UserCRUD(object):
AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
)
from api.lib.common_setting.employee import EmployeeCRUD
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
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']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
return user

View File

@@ -93,6 +93,9 @@ def _auth_with_token():
def _auth_with_ip_white_list():
if request.url.endswith("acl/users/info"):
return False
ip = request.headers.get('X-Real-IP') or request.remote_addr
key = request.values.get('_key')
secret = request.values.get('_secret')

View File

@@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@@ -15,7 +15,7 @@ try:
except ImportError:
from flask import _request_ctx_stack as stack
from api.flask_cas import routing
from . import routing
class CAS(object):

View File

@@ -119,4 +119,4 @@ def create_cas_validate_url(cas_url, cas_route, service, ticket,
('service', service),
('ticket', ticket),
('renew', renew),
)
)

View File

@@ -1,14 +1,24 @@
# -*- coding:utf-8 -*-
import json
import datetime
import uuid
import bs4
from flask import Blueprint
from flask import current_app, session, request, url_for, redirect
from flask_login import login_user, logout_user
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib.parse import urlparse
from six.moves.urllib_request import urlopen
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_url
from .cas_urls import create_cas_validate_url
@@ -16,6 +26,7 @@ from .cas_urls import create_cas_validate_url
blueprint = Blueprint('cas', __name__)
@blueprint.route('/api/cas/login')
@blueprint.route('/api/sso/login')
def login():
"""
@@ -29,16 +40,20 @@ def login():
If validation was successful the logged in username is saved in
the user's session under the key `CAS_USERNAME_SESSION_KEY`.
"""
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
if request.values.get("next"):
session["next"] = request.values.get("next")
_service = url_for('cas.login', _external=True, next=session["next"]) \
if session.get("next") else url_for('cas.login', _external=True)
# _service = url_for('cas.login', _external=True)
_service = "{}://{}{}".format(urlparse(request.referrer).scheme,
urlparse(request.referrer).netloc,
url_for('cas.login'))
redirect_url = create_cas_login_url(
current_app.config['CAS_SERVER'],
current_app.config['CAS_LOGIN_ROUTE'],
config['cas_server'],
config['cas_login_route'],
_service)
if 'ticket' in request.args:
@@ -47,30 +62,38 @@ def login():
if request.args.get('ticket'):
if validate(request.args['ticket']):
redirect_url = session.get("next") or \
current_app.config.get("CAS_AFTER_LOGIN")
redirect_url = session.get("next") or config.get("cas_after_login") or "/"
username = session.get("CAS_USERNAME")
user = UserCache.get(username)
login_user(user)
session.permanent = True
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else:
del session[cas_token_session_key]
redirect_url = create_cas_login_url(
current_app.config['CAS_SERVER'],
current_app.config['CAS_LOGIN_ROUTE'],
config['cas_server'],
config['cas_login_route'],
url_for('cas.login', _external=True),
renew=True)
AuditCRUD.add_login_log(session.get("CAS_USERNAME"), False, ErrFormat.invalid_password)
current_app.logger.info("redirect to: {0}".format(redirect_url))
return redirect(redirect_url)
@blueprint.route('/api/cas/logout')
@blueprint.route('/api/sso/logout')
def logout():
"""
When the user accesses this route they are logged out.
"""
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
current_app.logger.info(config)
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
@@ -82,12 +105,14 @@ def logout():
"next" in session and session.pop("next")
redirect_url = create_cas_logout_url(
current_app.config['CAS_SERVER'],
current_app.config['CAS_LOGOUT_ROUTE'],
config['cas_server'],
config['cas_logout_route'],
url_for('cas.login', _external=True, next=request.referrer))
logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
return redirect(redirect_url)
@@ -100,14 +125,15 @@ def validate(ticket):
and the validated username is saved in the session under the
key `CAS_USERNAME_SESSION_KEY`.
"""
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
current_app.logger.debug("validating token {0}".format(ticket))
cas_validate_url = create_cas_validate_url(
current_app.config['CAS_VALIDATE_SERVER'],
current_app.config['CAS_VALIDATE_ROUTE'],
config['cas_validate_server'],
config['cas_validate_route'],
url_for('cas.login', _external=True),
ticket)
@@ -115,23 +141,35 @@ def validate(ticket):
try:
response = urlopen(cas_validate_url).read()
ticketid = _parse_tag(response, "cas:user")
strs = [s.strip() for s in ticketid.split('|') if s.strip()]
ticket_id = _parse_tag(response, "cas:user")
strs = [s.strip() for s in ticket_id.split('|') if s.strip()]
username, is_valid = None, False
if len(strs) == 1:
username = strs[0]
is_valid = True
user_info = json.loads(_parse_tag(response, "cas:other"))
current_app.logger.info(user_info)
except ValueError:
current_app.logger.error("CAS returned unexpected result")
is_valid = False
return is_valid
if is_valid:
current_app.logger.debug("valid")
current_app.logger.debug("{}: {}".format(cas_username_session_key, username))
session[cas_username_session_key] = username
user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
soup = bs4.BeautifulSoup(response)
cas_user_map = config.get('cas_user_map')
user_dict = dict()
for k in cas_user_map:
v = soup.find(cas_user_map[k]['tag'], cas_user_map[k].get('attrs', {}))
user_dict[k] = v and v.text or None
user_dict['password'] = uuid.uuid4().hex
if "email" not in user_dict:
user_dict['email'] = username
UserCRUD.add(**user_dict)
from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username)
@@ -164,4 +202,5 @@ def _parse_tag(string, tag):
if soup.find(tag) is None:
return ''
return soup.find(tag).string.strip()

View File

@@ -0,0 +1,67 @@
# -*- coding:utf-8 -*-
import uuid
from flask import abort
from flask import current_app
from flask import session
from ldap3 import ALL
from ldap3 import AUTO_BIND_NO_TLS
from ldap3 import Connection
from ldap3 import Server
from ldap3.core.exceptions import LDAPBindError
from ldap3.core.exceptions import LDAPCertificateError
from ldap3.core.exceptions import LDAPSocketOpenError
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.resp_format import ErrFormat
from api.models.acl import User
def authenticate_with_ldap(username, password):
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
server = Server(config.get('ldap_server'), get_info=ALL, connect_timeout=3)
if '@' in username:
email = username
who = config.get('ldap_user_dn').format(username.split('@')[0])
else:
who = config.get('ldap_user_dn').format(username)
email = "{}@{}".format(who, config.get('ldap_domain'))
username = username.split('@')[0]
user = User.query.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
try:
conn = Connection(server, user=who, password=password, auto_bind=AUTO_BIND_NO_TLS)
except LDAPBindError:
conn = Connection(server,
user=f"{username}@{config.get('ldap_domain')}",
password=password,
auto_bind=AUTO_BIND_NO_TLS)
if conn.result['result'] != 0:
AuditCRUD.add_login_log(username, False, ErrFormat.invalid_password)
raise LDAPBindError
else:
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
if not user:
from api.lib.perm.acl.user import UserCRUD
user = UserCRUD.add(username=username, email=email, password=uuid.uuid4().hex)
return user, True
except LDAPBindError as e:
current_app.logger.info(e)
return user, False
except LDAPSocketOpenError as e:
current_app.logger.info(e)
return abort(403, ErrFormat.ldap_connection_failed)

View File

@@ -0,0 +1,30 @@
# -*- coding:utf-8 -*-
from flask import current_app
from . import routing
class OAuth2(object):
def __init__(self, app=None, url_prefix=None):
self._app = app
if app is not None:
self.init_app(app, url_prefix)
@staticmethod
def init_app(app, url_prefix=None):
# Configuration defaults
app.config.setdefault('OAUTH2_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
app.config.setdefault('OAUTH2_AFTER_LOGIN', '/')
app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OIDC_RESPONSE_TYPE', 'code')
app.config.setdefault('OIDC_AFTER_LOGIN', '/')
# Register Blueprint
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
@property
def app(self):
return self._app or current_app

View File

@@ -0,0 +1,139 @@
# -*- coding:utf-8 -*-
import datetime
import secrets
import uuid
import requests
from flask import Blueprint
from flask import abort
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib.parse import urlencode
from six.moves.urllib.parse import urlparse
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
blueprint = Blueprint('oauth2', __name__)
@blueprint.route('/api/<string:auth_type>/login')
def login(auth_type):
config = AuthenticateDataCRUD(auth_type.upper()).get()
if request.values.get("next"):
session["next"] = request.values.get("next")
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,
url_for('oauth2.callback', auth_type=auth_type.lower()))
qs = urlencode({
'client_id': config['client_id'],
'redirect_uri': redirect_uri,
'response_type': current_app.config[f'{auth_type}_RESPONSE_TYPE'],
'scope': ' '.join(config['scopes'] or []),
'state': session[f'{auth_type.lower()}_state'],
})
return redirect("{}?{}".format(config['authorize_url'].split('?')[0], qs))
@blueprint.route('/api/<string:auth_type>/callback')
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 '/'
if request.values['state'] != session.get(f'{auth_type.lower()}_state'):
return abort(401, "state is invalid")
if 'code' not in request.values:
return abort(401, 'code is invalid')
response = requests.post(config['token_url'], data={
'client_id': config['client_id'],
'client_secret': config['client_secret'],
'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'})
if response.status_code != 200:
current_app.logger.error(response.text)
return abort(401)
access_token = response.json().get('access_token')
if not access_token:
return abort(401)
response = requests.get(config['user_info']['url'], headers={
'Authorization': 'Bearer {}'.format(access_token),
'Accept': 'application/json',
})
if response.status_code != 200:
return abort(401)
res = response.json()
email = res.get(config['user_info']['email'])
username = res.get(config['user_info']['username'])
avatar = res.get(config['user_info'].get('avatar'))
user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
user_dict = dict(username=username, email=email, avatar=avatar)
user_dict['password'] = uuid.uuid4().hex
user = UserCRUD.add(**user_dict)
# log the user in
login_user(user)
from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username)
session["acl"] = dict(uid=user_info.get("uid"),
avatar=user.avatar if user else user_info.get("avatar"),
userId=user_info.get("uid"),
rid=user_info.get("rid"),
userName=user_info.get("username"),
nickName=user_info.get("nickname") or user_info.get("username"),
parentRoles=user_info.get("parents"),
childRoles=user_info.get("children"),
roleName=user_info.get("role"))
session["uid"] = user_info.get("uid")
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
return redirect(redirect_url)
@blueprint.route('/api/<string:auth_type>/logout')
def logout(auth_type):
"acl" in session and session.pop("acl")
"uid" in session and session.pop("uid")
f'{auth_type}_state' in session and session.pop(f'{auth_type}_state')
"next" in session and session.pop("next")
redirect_url = url_for('oauth2.login', auth_type=auth_type, _external=True, next=request.referrer)
logout_user()
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
return redirect(redirect_url)

View File

@@ -5,17 +5,18 @@ import copy
import hashlib
from datetime import datetime
from ldap3 import Server, Connection, ALL
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
from flask import current_app
from flask import session
from flask_sqlalchemy import BaseQuery
from api.extensions import db
from api.lib.database import CRUDModel
from api.lib.database import Model
from api.lib.database import Model2
from api.lib.database import SoftDeleteMixin
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.const import OperateType
from api.lib.perm.acl.resp_format import ErrFormat
class App(Model):
@@ -28,21 +29,26 @@ class App(Model):
class UserQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(UserQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password):
from api.lib.perm.acl.audit import AuditCRUD
user = self.filter(db.or_(User.username == login,
User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first()
if user:
current_app.logger.info(user)
authenticated = user.check_password(password)
if authenticated:
from api.tasks.acl import op_record
op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
_id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else:
AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password)
else:
authenticated = False
AuditCRUD.add_login_log(login, False, ErrFormat.user_not_found.format(login))
current_app.logger.info(("login", login, user, authenticated))
return user, authenticated
def authenticate_with_key(self, key, secret, args, path):
@@ -57,38 +63,6 @@ class UserQuery(BaseQuery):
return user, authenticated
def authenticate_with_ldap(self, username, password):
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
if '@' in username:
email = username
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
else:
who = current_app.config.get('LDAP_USER_DN').format(username)
email = "{}@{}".format(who, current_app.config.get('LDAP_DOMAIN'))
username = username.split('@')[0]
user = self.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
conn = Connection(server, user=who, password=password)
conn.bind()
if conn.result['result'] != 0:
raise LDAPBindError
conn.unbind()
if not user:
from api.lib.perm.acl.user import UserCRUD
user = UserCRUD.add(username=username, email=email)
from api.tasks.acl import op_record
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
return user, True
except LDAPBindError:
return user, False
def search(self, key):
query = self.filter(db.or_(User.email == key,
User.nickname.ilike('%' + key + '%'),
@@ -138,6 +112,7 @@ class User(CRUDModel, SoftDeleteMixin):
wx_id = db.Column(db.String(32))
employee_id = db.Column(db.String(16), index=True)
avatar = db.Column(db.String(128))
# apps = db.Column(db.JSON)
def __str__(self):
@@ -168,11 +143,9 @@ class User(CRUDModel, SoftDeleteMixin):
class RoleQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(RoleQuery, self)._join(*args, **kwargs)
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)
@@ -377,3 +350,16 @@ class AuditTriggerLog(Model):
current = db.Column(db.JSON, default=dict(), comment='当前数据')
extra = db.Column(db.JSON, default=dict(), comment='权限名')
source = db.Column(db.String(16), default='', comment='来源')
class AuditLoginLog(Model2):
__tablename__ = "acl_audit_login_logs"
username = db.Column(db.String(64), index=True)
channel = db.Column(db.Enum('web', 'api'), default="web")
ip = db.Column(db.String(15))
browser = db.Column(db.String(256))
description = db.Column(db.String(128))
is_ok = db.Column(db.Boolean)
login_at = db.Column(db.DateTime)
logout_at = db.Column(db.DateTime)

View File

@@ -96,3 +96,11 @@ class NoticeConfig(Model):
platform = db.Column(db.VARCHAR(255), nullable=False)
info = db.Column(db.JSON)
class CommonFile(Model):
__tablename__ = 'common_file'
file_name = db.Column(db.VARCHAR(512), nullable=False, index=True)
origin_name = db.Column(db.VARCHAR(512), nullable=False)
binary = db.Column(db.LargeBinary(16777216), nullable=False)

View File

@@ -25,10 +25,9 @@ from api.models.acl import Role
from api.models.acl import Trigger
@celery.task(base=QueueOnce,
name="acl.role_rebuild",
queue=ACL_QUEUE,
once={"graceful": True, "unlock_before_run": True})
@celery.task(name="acl.role_rebuild",
queue=ACL_QUEUE,)
@flush_db
@reconnect_db
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
@@ -190,18 +189,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)

View File

@@ -1,24 +1,24 @@
# -*- coding:utf-8 -*-
import requests
from flask import current_app
from api.extensions import celery
from api.extensions import db
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import COMMON_SETTING_QUEUE
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Department
from api.models.common_setting import Department, Employee
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=COMMON_SETTING_QUEUE)
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
"""
:param e_list:{acl_rid: 11, department_id: 22}
:param new_d_id
:param op_uid
"""
db.session.remove()
result = []
new_department = Department.get_by(
first=True, department_id=new_d_id, to_dict=False)
@@ -75,3 +75,41 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
result.append(ErrFormat.acl_add_user_to_role_failed.format(str(e)))
return result
@celery.task(name="common_setting.refresh_employee_acl_info", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def refresh_employee_acl_info():
acl = ACLManager('acl')
role_map = {role['name']: role for role in acl.get_all_roles()}
criterion = [
Employee.deleted == 0
]
query = Employee.query.filter(*criterion).order_by(
Employee.created_at.desc()
)
for em in query.all():
if em.acl_uid and em.acl_rid:
continue
role = role_map.get(em.username, None)
if not role:
continue
params = dict()
if not em.acl_uid:
params['acl_uid'] = role.get('uid', 0)
if not em.acl_rid:
params['acl_rid'] = role.get('id', 0)
try:
em.update(**params)
current_app.logger.info(
f"refresh_employee_acl_info success, employee_id: {em.employee_id}, uid: {em.acl_uid}, "
f"rid: {em.acl_rid}")
except Exception as e:
current_app.logger.error(str(e))
continue

View File

@@ -24,6 +24,7 @@ class AuditLogView(APIView):
'role': AuditCRUD.search_role,
'trigger': AuditCRUD.search_trigger,
'resource': AuditCRUD.search_resource,
'login': AuditCRUD.search_login,
}
if name not in func_map:
abort(400, f'wrong {name}, please use {func_map.keys()}')

View File

@@ -8,11 +8,15 @@ from flask import abort
from flask import current_app
from flask import request
from flask import session
from flask_login import login_user, logout_user
from flask_login import login_user
from flask_login import logout_user
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import User
from api.lib.perm.acl.cache import UserCache
@@ -34,8 +38,10 @@ class LoginView(APIView):
username = request.values.get("username") or request.values.get("email")
password = request.values.get("password")
_role = None
if current_app.config.get('AUTH_WITH_LDAP'):
user, authenticated = User.query.authenticate_with_ldap(username, password)
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
if config.get('enabled') or config.get('enable'):
from api.lib.perm.authentication.ldap import authenticate_with_ldap
user, authenticated = authenticate_with_ldap(username, password)
else:
user, authenticated = User.query.authenticate(username, password)
if not user:
@@ -176,4 +182,7 @@ class LogoutView(APIView):
@auth_abandoned
def post(self):
logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
self.jsonify(code=200)

View File

@@ -43,9 +43,11 @@ class CIRelationSearchView(APIView):
facet = handle_arg_list(request.values.get("facet", ""))
sort = request.values.get("sort")
reverse = request.values.get("reverse") in current_app.config.get('BOOL_TRUE')
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time()
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, ancestor_ids=ancestor_ids)
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse,
ancestor_ids=ancestor_ids, has_m2m=has_m2m)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:
@@ -69,9 +71,10 @@ class CIRelationStatisticsView(APIView):
level = request.values.get('level', 1)
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
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time()
s = Search(root_ids, level, ancestor_ids=ancestor_ids)
s = Search(root_ids, level, ancestor_ids=ancestor_ids, has_m2m=has_m2m)
try:
result = s.statistics(type_ids)
except SearchError as e:

View File

@@ -166,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)
@@ -318,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
@@ -457,13 +460,21 @@ 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)
if request.values.get('ci_filter') or request.values.get('attr_filter'):
CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
else:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
app_id = AppCache.get('cmdb').id
current_app.logger.info((rid, app_id))
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
current_app.logger.info('done')
return self.jsonify(code=200)
@@ -481,21 +492,27 @@ 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)
if PermEnum.READ in perms:
CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)
app_id = AppCache.get('cmdb').id
resource = None
if PermEnum.READ in perms or not perms:
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=(rid, app_id), queue=ACL_QUEUE)
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'))
return self.jsonify(type_id=type_id, rid=rid)

View File

@@ -0,0 +1,88 @@
from flask import abort, request
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import TestType
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.acl import role_required
from api.resource import APIView
prefix = '/auth_config'
class AuthConfigView(APIView):
url_prefix = (f'{prefix}/<string:auth_type>',)
@role_required("acl_admin")
def get(self, auth_type):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
if auth_type in cli.common_type_list:
data = cli.get_record(True)
else:
data = cli.get_record_with_decrypt()
return self.jsonify(data)
@role_required("acl_admin")
def post(self, auth_type):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
params = request.json
data = params.get('data', {})
if auth_type in cli.common_type_list:
data['encrypt'] = False
cli.create(data)
return self.jsonify(params)
class AuthConfigViewWithId(APIView):
url_prefix = (f'{prefix}/<string:auth_type>/<int:_id>',)
@role_required("acl_admin")
def put(self, auth_type, _id):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
params = request.json
data = params.get('data', {})
if auth_type in cli.common_type_list:
data['encrypt'] = False
res = cli.update(_id, data)
return self.jsonify(res.to_dict())
@role_required("acl_admin")
def delete(self, auth_type, _id):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
cli.delete(_id)
return self.jsonify({})
class AuthEnableListView(APIView):
url_prefix = (f'{prefix}/enable_list',)
method_decorators = []
def get(self):
return self.jsonify(AuthenticateDataCRUD.get_enable_list())
class AuthConfigTestView(APIView):
url_prefix = (f'{prefix}/<string:auth_type>/test',)
def post(self, auth_type):
test_type = request.values.get('test_type', TestType.Connect)
params = request.json
return self.jsonify(AuthenticateDataCRUD(auth_type).test(test_type, params.get('data')))

View File

@@ -24,12 +24,12 @@ class DataView(APIView):
class DataViewWithId(APIView):
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
def put(self, _id):
def put(self, data_type, _id):
params = request.json
res = CommonDataCRUD.update_data(_id, **params)
return self.jsonify(res.to_dict())
def delete(self, _id):
def delete(self, data_type, _id):
CommonDataCRUD.delete(_id)
return self.jsonify({})

View File

@@ -3,9 +3,10 @@ import os
from flask import request, abort, current_app, send_from_directory
from werkzeug.utils import secure_filename
import lz4.frame
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name
from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name, CommonFileCRUD
from api.resource import APIView
prefix = '/file'
@@ -28,7 +29,8 @@ class GetFileView(APIView):
url_prefix = (f'{prefix}/<string:_filename>',)
def get(self, _filename):
return send_from_directory(current_app.config['UPLOAD_DIRECTORY_FULL'], _filename, as_attachment=True)
file_stream = CommonFileCRUD.get_file(_filename)
return self.send_file(file_stream, as_attachment=True, download_name=_filename)
class PostFileView(APIView):
@@ -44,6 +46,8 @@ class PostFileView(APIView):
if not file:
abort(400, ErrFormat.file_is_required)
extension = file.mimetype.split('/')[-1]
if '+' in extension:
extension = file.filename.split('.')[-1]
if file.filename == '':
filename = f'.{extension}'
else:
@@ -53,11 +57,20 @@ class PostFileView(APIView):
filename = file.filename
if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)):
filename = generate_new_file_name(filename)
filename = secure_filename(filename)
file.save(os.path.join(
current_app.config['UPLOAD_DIRECTORY_FULL'], filename))
new_filename = generate_new_file_name(filename)
new_filename = secure_filename(new_filename)
file_content = file.read()
compressed_data = lz4.frame.compress(file_content)
try:
CommonFileCRUD.add_file(
origin_name=filename,
file_name=new_filename,
binary=compressed_data,
)
return self.jsonify(file_name=filename)
return self.jsonify(file_name=new_filename)
except Exception as e:
current_app.logger.error(e)
abort(400, ErrFormat.upload_failed.format(e))
abort(400, 'Extension not allow')
abort(400, ErrFormat.file_type_not_allowed.format(filename))

View File

@@ -34,7 +34,7 @@ cryptography>=41.0.2
PyJWT==2.4.0
PyMySQL==1.1.0
ldap3==2.9.1
PyYAML==6.0
PyYAML==6.0.1
redis==4.6.0
requests==2.31.0
requests_oauthlib==1.3.1
@@ -48,6 +48,6 @@ treelib==1.6.1
Werkzeug>=2.3.6
WTForms==3.0.0
shamir~=17.12.0
hvac~=2.0.0
pycryptodomex>=3.19.0
colorama>=0.4.6
lz4>=4.3.2

View File

@@ -11,10 +11,10 @@ from environs import Env
env = Env()
env.read_env()
ENV = env.str("FLASK_ENV", default="production")
DEBUG = ENV == "development"
SECRET_KEY = env.str("SECRET_KEY")
BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13)
ENV = env.str('FLASK_ENV', default='production')
DEBUG = ENV == 'development'
SECRET_KEY = env.str('SECRET_KEY')
BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13)
DEBUG_TB_ENABLED = DEBUG
DEBUG_TB_INTERCEPT_REDIRECTS = False
@@ -23,7 +23,7 @@ ERROR_CODES = [400, 401, 403, 404, 405, 500, 502]
# # database
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
SQLALCHEMY_BINDS = {
"user": 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
'user': 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
}
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -32,11 +32,11 @@ SQLALCHEMY_ENGINE_OPTIONS = {
}
# # cache
CACHE_TYPE = "redis"
CACHE_REDIS_HOST = "127.0.0.1"
CACHE_TYPE = 'redis'
CACHE_REDIS_HOST = '127.0.0.1'
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = ""
CACHE_KEY_PREFIX = "CMDB::"
CACHE_REDIS_PASSWORD = ''
CACHE_KEY_PREFIX = 'CMDB::'
CACHE_DEFAULT_TIMEOUT = 3000
# # log
@@ -55,10 +55,10 @@ DEFAULT_MAIL_SENDER = ''
# # queue
CELERY = {
"broker_url": 'redis://127.0.0.1:6379/2',
"result_backend": "redis://127.0.0.1:6379/2",
"broker_vhost": "/",
"broker_connection_retry_on_startup": True
'broker_url': 'redis://127.0.0.1:6379/2',
'result_backend': 'redis://127.0.0.1:6379/2',
'broker_vhost': '/',
'broker_connection_retry_on_startup': True
}
ONCE = {
'backend': 'celery_once.backends.Redis',
@@ -67,33 +67,81 @@ ONCE = {
}
}
# # SSO
CAS_SERVER = "http://sso.xxx.com"
CAS_VALIDATE_SERVER = "http://sso.xxx.com"
CAS_LOGIN_ROUTE = "/cas/login"
CAS_LOGOUT_ROUTE = "/cas/logout"
CAS_VALIDATE_ROUTE = "/cas/serviceValidate"
CAS_AFTER_LOGIN = "/"
DEFAULT_SERVICE = "http://127.0.0.1:8000"
# =============================== Authentication ===========================================================
# # ldap
AUTH_WITH_LDAP = False
LDAP_SERVER = ''
LDAP_DOMAIN = ''
LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com'
# # CAS
CAS = dict(
enabled=False,
cas_server='https://{your-CASServer-hostname}',
cas_validate_server='https://{your-CASServer-hostname}',
cas_login_route='/cas/built-in/cas/login',
cas_logout_route='/cas/built-in/cas/logout',
cas_validate_route='/cas/built-in/cas/serviceValidate',
cas_after_login='/',
cas_user_map={
'username': {'tag': 'cas:user'},
'nickname': {'tag': 'cas:attribute', 'attrs': {'name': 'displayName'}},
'email': {'tag': 'cas:attribute', 'attrs': {'name': 'email'}},
'mobile': {'tag': 'cas:attribute', 'attrs': {'name': 'phone'}},
'avatar': {'tag': 'cas:attribute', 'attrs': {'name': 'avatar'}},
}
)
# # OAuth2.0
OAUTH2 = dict(
enabled=False,
client_id='',
client_secret='',
authorize_url='https://{your-OAuth2Server-hostname}/login/oauth/authorize',
token_url='https://{your-OAuth2Server-hostname}/api/login/oauth/access_token',
scopes=['profile', 'email'],
user_info={
'url': 'https://{your-OAuth2Server-hostname}/api/userinfo',
'email': 'email',
'username': 'name',
'avatar': 'picture'
},
after_login='/'
)
# # OIDC
OIDC = dict(
enabled=False,
client_id='',
client_secret='',
authorize_url='https://{your-OIDCServer-hostname}/login/oauth/authorize',
token_url='https://{your-OIDCServer-hostname}/api/login/oauth/access_token',
scopes=['openid', 'profile', 'email'],
user_info={
'url': 'https://{your-OIDCServer-hostname}/api/userinfo',
'email': 'email',
'username': 'name',
'avatar': 'picture'
},
after_login='/'
)
# # LDAP
LDAP = dict(
enabled=False,
ldap_server='',
ldap_domain='',
ldap_user_dn='cn={},ou=users,dc=xxx,dc=com'
)
# ==========================================================================================================
# # pagination
DEFAULT_PAGE_COUNT = 50
# # permission
WHITE_LIST = ["127.0.0.1"]
WHITE_LIST = ['127.0.0.1']
USE_ACL = True
# # elastic search
ES_HOST = '127.0.0.1'
USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, 'Yes', 'YES', 'yes', 'Y', 'y']
# # messenger
USE_MESSENGER = True

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1698273699449') format('truetype');
src: url('iconfont.woff2?t=1702544951995') format('woff2'),
url('iconfont.woff?t=1702544951995') format('woff'),
url('iconfont.ttf?t=1702544951995') format('truetype');
}
.iconfont {
@@ -13,6 +13,274 @@
-moz-osx-font-smoothing: grayscale;
}
.OAUTH2:before {
content: "\e8d8";
}
.OIDC:before {
content: "\e8d6";
}
.CAS:before {
content: "\e8d7";
}
.ops-setting-auth:before {
content: "\e8d5";
}
.ops-setting-auth-selected:before {
content: "\e8d4";
}
.a-itsm-knowledge2:before {
content: "\e8d2";
}
.itsm-qrdownload:before {
content: "\e8d3";
}
.oneterm-playback:before {
content: "\e8d1";
}
.oneterm-disconnect:before {
content: "\e8d0";
}
.ops-oneterm-publickey-selected:before {
content: "\e8cf";
}
.ops-oneterm-publickey:before {
content: "\e8ce";
}
.ops-oneterm-gateway:before {
content: "\e8b9";
}
.ops-oneterm-gateway-selected:before {
content: "\e8bf";
}
.ops-oneterm-account:before {
content: "\e8c0";
}
.ops-oneterm-account-selected:before {
content: "\e8c1";
}
.ops-oneterm-command:before {
content: "\e8c2";
}
.ops-oneterm-command-selected:before {
content: "\e8c3";
}
.ops-oneterm-assetlist:before {
content: "\e8c4";
}
.ops-oneterm-assetlist-selected:before {
content: "\e8c5";
}
.ops-oneterm-sessiononline:before {
content: "\e8c6";
}
.ops-oneterm-sessiononline-selected:before {
content: "\e8c7";
}
.ops-oneterm-sessionhistory-selected:before {
content: "\e8c8";
}
.ops-oneterm-sessionhistory:before {
content: "\e8c9";
}
.ops-oneterm-login:before {
content: "\e8ca";
}
.ops-oneterm-login-selected:before {
content: "\e8cb";
}
.ops-oneterm-operation:before {
content: "\e8cc";
}
.ops-oneterm-operation-selected:before {
content: "\e8cd";
}
.ops-oneterm-workstation-selected:before {
content: "\e8b7";
}
.ops-oneterm-workstation:before {
content: "\e8b8";
}
.oneterm-file-selected:before {
content: "\e8be";
}
.oneterm-file:before {
content: "\e8bc";
}
.oneterm-time:before {
content: "\e8bd";
}
.oneterm-download:before {
content: "\e8bb";
}
.oneterm-commandrecord:before {
content: "\e8ba";
}
.oneterm-asset:before {
content: "\e8b6";
}
.oneterm-total_asset:before {
content: "\e8b5";
}
.oneterm-switch:before {
content: "\e8b4";
}
.oneterm-session:before {
content: "\e8b3";
}
.oneterm-connect:before {
content: "\e8b2";
}
.oneterm-login:before {
content: "\e8b1";
}
.ops-oneterm-dashboard:before {
content: "\e8af";
}
.ops-oneterm-dashboard-selected:before {
content: "\e8b0";
}
.oneterm-recentsession:before {
content: "\e8ae";
}
.oneterm-myassets:before {
content: "\e8ad";
}
.ops-oneterm-log:before {
content: "\e8aa";
}
.ops-oneterm-session-selected:before {
content: "\e8ab";
}
.ops-oneterm-session:before {
content: "\e8ac";
}
.ops-oneterm-log-selected:before {
content: "\e8a9";
}
.ops-oneterm-assets:before {
content: "\e8a7";
}
.ops-oneterm-assets-selected:before {
content: "\e8a8";
}
.itsm-down:before {
content: "\e8a5";
}
.itsm-up:before {
content: "\e8a6";
}
.itsm-download:before {
content: "\e8a4";
}
.itsm-print:before {
content: "\e8a3";
}
.itsm-view:before {
content: "\e8a2";
}
.itsm-word:before {
content: "\e8a1";
}
.datainsight-custom:before {
content: "\e89e";
}
.datainsight-prometheus:before {
content: "\e89f";
}
.datainsight-zabbix:before {
content: "\e8a0";
}
.setting-mainpeople:before {
content: "\e89a";
}
.setting-deputypeople:before {
content: "\e89d";
}
.ops-setting-duty:before {
content: "\e89c";
}
.ops-setting-duty-selected:before {
content: "\e89b";
}
.datainsight-sequential:before {
content: "\e899";
}
.datainsight-close:before {
content: "\e898";
}
.datainsight-handle:before {
content: "\e897";
}
.datainsight-table:before {
content: "\e896";
}
.icon-xianxing-password:before {
content: "\e894";
}
@@ -21,11 +289,11 @@
content: "\e895";
}
.a-itsm-oneclickdownload:before {
.itsm-download-all:before {
content: "\e892";
}
.a-itsm-packagedownload:before {
.itsm-download-package:before {
content: "\e893";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,475 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "38566548",
"name": "OAuth2.0",
"font_class": "OAUTH2",
"unicode": "e8d8",
"unicode_decimal": 59608
},
{
"icon_id": "38566584",
"name": "OIDC",
"font_class": "OIDC",
"unicode": "e8d6",
"unicode_decimal": 59606
},
{
"icon_id": "38566578",
"name": "cas",
"font_class": "CAS",
"unicode": "e8d7",
"unicode_decimal": 59607
},
{
"icon_id": "38547395",
"name": "setting-authentication",
"font_class": "ops-setting-auth",
"unicode": "e8d5",
"unicode_decimal": 59605
},
{
"icon_id": "38547389",
"name": "setting-authentication-selected",
"font_class": "ops-setting-auth-selected",
"unicode": "e8d4",
"unicode_decimal": 59604
},
{
"icon_id": "38533133",
"name": "itsm-knowledge (2)",
"font_class": "a-itsm-knowledge2",
"unicode": "e8d2",
"unicode_decimal": 59602
},
{
"icon_id": "38531868",
"name": "itsm-QRcode",
"font_class": "itsm-qrdownload",
"unicode": "e8d3",
"unicode_decimal": 59603
},
{
"icon_id": "38413515",
"name": "oneterm-playback",
"font_class": "oneterm-playback",
"unicode": "e8d1",
"unicode_decimal": 59601
},
{
"icon_id": "38413481",
"name": "oneterm-disconnect",
"font_class": "oneterm-disconnect",
"unicode": "e8d0",
"unicode_decimal": 59600
},
{
"icon_id": "38407867",
"name": "oneterm-key-selected",
"font_class": "ops-oneterm-publickey-selected",
"unicode": "e8cf",
"unicode_decimal": 59599
},
{
"icon_id": "38407915",
"name": "oneterm-key",
"font_class": "ops-oneterm-publickey",
"unicode": "e8ce",
"unicode_decimal": 59598
},
{
"icon_id": "38311855",
"name": "oneterm-gateway",
"font_class": "ops-oneterm-gateway",
"unicode": "e8b9",
"unicode_decimal": 59577
},
{
"icon_id": "38311938",
"name": "oneterm-gateway-selected",
"font_class": "ops-oneterm-gateway-selected",
"unicode": "e8bf",
"unicode_decimal": 59583
},
{
"icon_id": "38311957",
"name": "oneterm-account",
"font_class": "ops-oneterm-account",
"unicode": "e8c0",
"unicode_decimal": 59584
},
{
"icon_id": "38311961",
"name": "oneterm-account-selected",
"font_class": "ops-oneterm-account-selected",
"unicode": "e8c1",
"unicode_decimal": 59585
},
{
"icon_id": "38311974",
"name": "oneterm-command",
"font_class": "ops-oneterm-command",
"unicode": "e8c2",
"unicode_decimal": 59586
},
{
"icon_id": "38311976",
"name": "oneterm-command-selected",
"font_class": "ops-oneterm-command-selected",
"unicode": "e8c3",
"unicode_decimal": 59587
},
{
"icon_id": "38311979",
"name": "oneterm-asset_list",
"font_class": "ops-oneterm-assetlist",
"unicode": "e8c4",
"unicode_decimal": 59588
},
{
"icon_id": "38311985",
"name": "oneterm-asset_list-selected",
"font_class": "ops-oneterm-assetlist-selected",
"unicode": "e8c5",
"unicode_decimal": 59589
},
{
"icon_id": "38312030",
"name": "oneterm-online",
"font_class": "ops-oneterm-sessiononline",
"unicode": "e8c6",
"unicode_decimal": 59590
},
{
"icon_id": "38312152",
"name": "oneterm-online-selected",
"font_class": "ops-oneterm-sessiononline-selected",
"unicode": "e8c7",
"unicode_decimal": 59591
},
{
"icon_id": "38312154",
"name": "oneterm-history-selected",
"font_class": "ops-oneterm-sessionhistory-selected",
"unicode": "e8c8",
"unicode_decimal": 59592
},
{
"icon_id": "38312155",
"name": "oneterm-history",
"font_class": "ops-oneterm-sessionhistory",
"unicode": "e8c9",
"unicode_decimal": 59593
},
{
"icon_id": "38312404",
"name": "oneterm-entry_log",
"font_class": "ops-oneterm-login",
"unicode": "e8ca",
"unicode_decimal": 59594
},
{
"icon_id": "38312423",
"name": "oneterm-entry_log-selected",
"font_class": "ops-oneterm-login-selected",
"unicode": "e8cb",
"unicode_decimal": 59595
},
{
"icon_id": "38312426",
"name": "oneterm-operation_log",
"font_class": "ops-oneterm-operation",
"unicode": "e8cc",
"unicode_decimal": 59596
},
{
"icon_id": "38312445",
"name": "oneterm-operation_log-selected",
"font_class": "ops-oneterm-operation-selected",
"unicode": "e8cd",
"unicode_decimal": 59597
},
{
"icon_id": "38307876",
"name": "oneterm-workstation-selected",
"font_class": "ops-oneterm-workstation-selected",
"unicode": "e8b7",
"unicode_decimal": 59575
},
{
"icon_id": "38307871",
"name": "oneterm-workstation",
"font_class": "ops-oneterm-workstation",
"unicode": "e8b8",
"unicode_decimal": 59576
},
{
"icon_id": "38302246",
"name": "oneterm-file-selected",
"font_class": "oneterm-file-selected",
"unicode": "e8be",
"unicode_decimal": 59582
},
{
"icon_id": "38302255",
"name": "oneterm-file",
"font_class": "oneterm-file",
"unicode": "e8bc",
"unicode_decimal": 59580
},
{
"icon_id": "38203528",
"name": "oneterm-time",
"font_class": "oneterm-time",
"unicode": "e8bd",
"unicode_decimal": 59581
},
{
"icon_id": "38203331",
"name": "oneterm-download",
"font_class": "oneterm-download",
"unicode": "e8bb",
"unicode_decimal": 59579
},
{
"icon_id": "38201351",
"name": "oneterm-command record",
"font_class": "oneterm-commandrecord",
"unicode": "e8ba",
"unicode_decimal": 59578
},
{
"icon_id": "38199341",
"name": "oneterm-connected assets",
"font_class": "oneterm-asset",
"unicode": "e8b6",
"unicode_decimal": 59574
},
{
"icon_id": "38199350",
"name": "oneterm-total assets",
"font_class": "oneterm-total_asset",
"unicode": "e8b5",
"unicode_decimal": 59573
},
{
"icon_id": "38199303",
"name": "oneterm-switch (3)",
"font_class": "oneterm-switch",
"unicode": "e8b4",
"unicode_decimal": 59572
},
{
"icon_id": "38199317",
"name": "oneterm-session",
"font_class": "oneterm-session",
"unicode": "e8b3",
"unicode_decimal": 59571
},
{
"icon_id": "38199339",
"name": "oneterm-connection",
"font_class": "oneterm-connect",
"unicode": "e8b2",
"unicode_decimal": 59570
},
{
"icon_id": "38198321",
"name": "oneterm-log in",
"font_class": "oneterm-login",
"unicode": "e8b1",
"unicode_decimal": 59569
},
{
"icon_id": "38194554",
"name": "oneterm-dashboard",
"font_class": "ops-oneterm-dashboard",
"unicode": "e8af",
"unicode_decimal": 59567
},
{
"icon_id": "38194525",
"name": "oneterm-dashboard-selected",
"font_class": "ops-oneterm-dashboard-selected",
"unicode": "e8b0",
"unicode_decimal": 59568
},
{
"icon_id": "38194352",
"name": "oneterm-recent session",
"font_class": "oneterm-recentsession",
"unicode": "e8ae",
"unicode_decimal": 59566
},
{
"icon_id": "38194383",
"name": "oneterm-my assets",
"font_class": "oneterm-myassets",
"unicode": "e8ad",
"unicode_decimal": 59565
},
{
"icon_id": "38194089",
"name": "oneterm-log",
"font_class": "ops-oneterm-log",
"unicode": "e8aa",
"unicode_decimal": 59562
},
{
"icon_id": "38194088",
"name": "oneterm-conversation-selected",
"font_class": "ops-oneterm-session-selected",
"unicode": "e8ab",
"unicode_decimal": 59563
},
{
"icon_id": "38194065",
"name": "oneterm-conversation",
"font_class": "ops-oneterm-session",
"unicode": "e8ac",
"unicode_decimal": 59564
},
{
"icon_id": "38194105",
"name": "oneterm-log-selected",
"font_class": "ops-oneterm-log-selected",
"unicode": "e8a9",
"unicode_decimal": 59561
},
{
"icon_id": "38194054",
"name": "oneterm-assets",
"font_class": "ops-oneterm-assets",
"unicode": "e8a7",
"unicode_decimal": 59559
},
{
"icon_id": "38194055",
"name": "oneterm-assets-selected",
"font_class": "ops-oneterm-assets-selected",
"unicode": "e8a8",
"unicode_decimal": 59560
},
{
"icon_id": "38123087",
"name": "itsm-down",
"font_class": "itsm-down",
"unicode": "e8a5",
"unicode_decimal": 59557
},
{
"icon_id": "38123084",
"name": "itsm-up",
"font_class": "itsm-up",
"unicode": "e8a6",
"unicode_decimal": 59558
},
{
"icon_id": "38105374",
"name": "itsm-download",
"font_class": "itsm-download",
"unicode": "e8a4",
"unicode_decimal": 59556
},
{
"icon_id": "38105235",
"name": "itsm-print",
"font_class": "itsm-print",
"unicode": "e8a3",
"unicode_decimal": 59555
},
{
"icon_id": "38104997",
"name": "itsm-view",
"font_class": "itsm-view",
"unicode": "e8a2",
"unicode_decimal": 59554
},
{
"icon_id": "38105129",
"name": "itsm-word",
"font_class": "itsm-word",
"unicode": "e8a1",
"unicode_decimal": 59553
},
{
"icon_id": "38095730",
"name": "datainsight-custom",
"font_class": "datainsight-custom",
"unicode": "e89e",
"unicode_decimal": 59550
},
{
"icon_id": "38095729",
"name": "datainsight-prometheus",
"font_class": "datainsight-prometheus",
"unicode": "e89f",
"unicode_decimal": 59551
},
{
"icon_id": "38095728",
"name": "datainsight-zabbix",
"font_class": "datainsight-zabbix",
"unicode": "e8a0",
"unicode_decimal": 59552
},
{
"icon_id": "37944507",
"name": "setting-main people",
"font_class": "setting-mainpeople",
"unicode": "e89a",
"unicode_decimal": 59546
},
{
"icon_id": "37944503",
"name": "setting-deputy people",
"font_class": "setting-deputypeople",
"unicode": "e89d",
"unicode_decimal": 59549
},
{
"icon_id": "37940080",
"name": "ops-setting-duty",
"font_class": "ops-setting-duty",
"unicode": "e89c",
"unicode_decimal": 59548
},
{
"icon_id": "37940033",
"name": "ops-setting-duty-selected",
"font_class": "ops-setting-duty-selected",
"unicode": "e89b",
"unicode_decimal": 59547
},
{
"icon_id": "37841524",
"name": "datainsight-sequential",
"font_class": "datainsight-sequential",
"unicode": "e899",
"unicode_decimal": 59545
},
{
"icon_id": "37841535",
"name": "datainsight-close",
"font_class": "datainsight-close",
"unicode": "e898",
"unicode_decimal": 59544
},
{
"icon_id": "37841537",
"name": "datainsight-handle",
"font_class": "datainsight-handle",
"unicode": "e897",
"unicode_decimal": 59543
},
{
"icon_id": "37841515",
"name": "datainsight-table",
"font_class": "datainsight-table",
"unicode": "e896",
"unicode_decimal": 59542
},
{
"icon_id": "37830610",
"name": "icon-xianxing-password",
@@ -22,14 +491,14 @@
{
"icon_id": "37822199",
"name": "itsm-oneclick download",
"font_class": "a-itsm-oneclickdownload",
"font_class": "itsm-download-all",
"unicode": "e892",
"unicode_decimal": 59538
},
{
"icon_id": "37822198",
"name": "itsm-package download",
"font_class": "a-itsm-packagedownload",
"font_class": "itsm-download-package",
"unicode": "e893",
"unicode_decimal": 59539
},

Binary file not shown.

39
cmdb-ui/src/api/auth.js Normal file
View File

@@ -0,0 +1,39 @@
import { axios } from '@/utils/request'
export function getAuthData(data_type) {
return axios({
url: `/common-setting/v1/auth_config/${data_type}`,
method: 'get',
})
}
export function postAuthData(data_type, data) {
return axios({
url: `/common-setting/v1/auth_config/${data_type}`,
method: 'post',
data,
})
}
export function putAuthData(data_type, id, data) {
return axios({
url: `/common-setting/v1/auth_config/${data_type}/${id}`,
method: 'put',
data,
})
}
export function getAuthDataEnable() {
return axios({
url: `/common-setting/v1/auth_config/enable_list`,
method: 'get',
})
}
export function testLDAP(test_type, data) {
return axios({
url: `/common-setting/v1/auth_config/LDAP/test?test_type=${test_type}`,
method: 'post',
data,
})
}

View File

@@ -1,8 +1,6 @@
import config from '@/config/setting'
const api = {
Login: config.useSSO ? '/api/sso/login' : '/v1/acl/login',
Logout: config.useSSO ? '/api/sso/logout' : '/v1/acl/logout',
Login: '/v1/acl/login',
Logout: '/v1/acl/logout',
ForgePassword: '/auth/forge-password',
Register: '/auth/register',
twoStepCode: '/auth/2step-code',

View File

@@ -1,6 +1,5 @@
import api from './index'
import { axios } from '@/utils/request'
import config from '@/config/setting'
/**
* login func
* parameter: {
@@ -12,9 +11,10 @@ import config from '@/config/setting'
* @param parameter
* @returns {*}
*/
export function login(data) {
if (config.useSSO) {
window.location.href = config.ssoLoginUrl
export function login(data, auth_type) {
if (auth_type) {
localStorage.setItem('ops_auth_type', auth_type)
window.location.href = `/api/${auth_type.toLowerCase()}/login`
} else {
return axios({
url: api.Login,
@@ -43,17 +43,15 @@ export function getInfo() {
}
export function logout() {
if (config.useSSO) {
window.location.replace(api.Logout)
} else {
return axios({
url: api.Logout,
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
const auth_type = localStorage.getItem('ops_auth_type')
localStorage.clear()
return axios({
url: auth_type ? `/${auth_type.toLowerCase()}/logout` : api.Logout,
method: auth_type ? 'get' : 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -95,6 +95,10 @@ export default {
const unsubTree = subscribeTreeView(citypeId, '')
Promise.all([unsubCIType, unsubTree]).then(() => {
that.$message.success('取消订阅成功')
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (Number(citypeId) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '')
}
// 删除路由
const href = window.location.href
const hrefSplit = href.split('/')

View File

@@ -66,10 +66,8 @@ export default {
this.$confirm({
title: '提示',
content: '真的要注销登录 ?',
content: '确认注销登录 ?',
onOk() {
// localStorage.removeItem('ops_cityps_currentId')
localStorage.clear()
return that.Logout()
},
onCancel() {},

View File

@@ -2,7 +2,6 @@ const appConfig = {
buildModules: ['cmdb', 'acl'], // 需要编译的模块
redirectTo: '/cmdb', // 首页的重定向路径
buildAclToModules: true, // 是否在各个应用下 内联权限管理
ssoLogoutURL: '/api/sso/logout',
showDocs: false,
useEncryption: false,
}

View File

@@ -1,6 +1,5 @@
/**
* 项目默认配置项
* useSSO - 是否启用单点登录, 默认为否, 可以根据需要接入到公司的单点登录系统
* primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage
* navTheme - sidebar theme ['dark', 'light'] 两种主题
* colorWeak - 色盲模式
@@ -15,8 +14,6 @@
*/
export default {
useSSO: false,
ssoLoginUrl: '/api/sso/login',
primaryColor: '#1890ff', // primary color of ant design
navTheme: 'dark', // theme for nav menu
layout: 'sidemenu', // nav menu position: sidemenu or topmenu

View File

@@ -6,7 +6,6 @@ import store from './store'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import config from '@/config/setting'
import { ACCESS_TOKEN } from './store/global/mutation-types'
NProgress.configure({ showSpinner: false })
@@ -16,16 +15,16 @@ const whitePath = ['/user/login', '/user/logout', '/user/register', '/api/sso/lo
// 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录
// 登录页面处理处理 是否使用单点登录
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
NProgress.start() // start progress bar
to.meta && (!!to.meta.title && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
const authed = store.state.authed
const auth_type = localStorage.getItem('ops_auth_type')
if (whitePath.includes(to.path)) {
next()
} else if ((config.useSSO || (!config.useSSO && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) {
} else if ((auth_type || (!auth_type && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) {
store.dispatch('GetAuthDataEnable')
store.dispatch('GetInfo').then(res => {
const roles = res.result && res.result.role
store.dispatch("loadAllUsers")
@@ -46,10 +45,17 @@ router.beforeEach((to, from, next) => {
}).catch((e) => {
setTimeout(() => { store.dispatch('Logout') }, 3000)
})
} else if (to.path === '/user/login' && !config.useSSO && store.getters.roles.length !== 0) {
} else if (to.path === '/user/login' && !auth_type && store.getters.roles.length !== 0) {
next({ path: '/' })
} else if (!config.useSSO && !Vue.ls.get(ACCESS_TOKEN) && to.path !== '/user/login') {
next({ path: '/user/login', query: { redirect: to.fullPath } })
} else if (!auth_type && !Vue.ls.get(ACCESS_TOKEN) && to.path !== '/user/login') {
await store.dispatch('GetAuthDataEnable')
const { enable_list = [] } = store?.state?.user?.auth_enable ?? {}
const _enable_list = enable_list.filter(en => en.auth_type !== 'LDAP')
if (_enable_list.length === 1) {
next({ path: '/user/logout', query: { redirect: to.fullPath } })
} else {
next({ path: '/user/login', query: { redirect: to.fullPath } })
}
} else {
next()
}

View File

@@ -65,8 +65,7 @@ const genAclRoutes = async () => {
path: `/acl/operate_history`,
name: 'acl_operate_history',
component: () => import('../views/operation_history/index.vue'),
// meta: { title: '操作审计', icon: 'search', permission: ['acl_admin'] },
meta: { title: '操作审计', icon: 'search' }
meta: { title: '操作审计', icon: 'search', permission: ['acl_admin'] },
},
{
path: `/acl/user`,

View File

@@ -133,8 +133,8 @@ export default {
if (newVal) {
this.tableData = this.allUsers.filter(
(item) =>
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
(item.username && item.username.toLowerCase().includes(newVal.toLowerCase())) ||
(item.nickname && item.nickname.toLowerCase().includes(newVal.toLowerCase()))
)
} else {
this.tableData = this.allUsers

View File

@@ -16,12 +16,14 @@ export function processFile(fileObj) {
}
export function uploadData(ciId, data) {
data.ci_type = ciId
data.exist_policy = 'replace'
return axios({
url: '/v0.1/ci',
method: 'POST',
data,
data: {
...data,
ci_type: ciId,
exist_policy: 'replace'
},
isShowMessage: false
})
}

View File

@@ -111,12 +111,14 @@ export default {
},
methods: {
...mapMutations('cmdbStore', ['SET_IS_TABLE_LOADING']),
open({ preferenceAttrList }) {
open({ preferenceAttrList, ciTypeName = undefined }) {
this.preferenceAttrList = preferenceAttrList
this.visible = true
this.$nextTick((res) => {
this.form.setFieldsValue({
filename: `cmdb-${moment().format('YYYYMMDDHHmmss')}`,
filename: ciTypeName
? `cmdb-${ciTypeName}-${moment().format('YYYYMMDDHHmmss')}`
: `cmdb-${moment().format('YYYYMMDDHHmmss')}`,
})
if (this.treeType === 'tree') {
const _check = ['ci_type_alias']

View File

@@ -77,7 +77,7 @@ const genCmdbRoutes = async () => {
path: '/cmdb/ci_types',
name: 'ci_type',
component: () => import('../views/ci_types/index'),
meta: { title: '模型配置', icon: 'ops-cmdb-citype', selectedIcon: 'ops-cmdb-citype-selected', keepAlive: false }
meta: { title: '模型配置', icon: 'ops-cmdb-citype', selectedIcon: 'ops-cmdb-citype-selected', keepAlive: false, permission: ['cmdb_admin', 'admin'] }
},
{
path: '/cmdb/disabled3',

View File

@@ -1,11 +1,16 @@
<template>
<div class="cmdb-batch-upload" :style="{ height: `${windowHeight - 64}px` }">
<div id="title">
<ci-type-choice @getCiTypeAttr="showCiType" />
<ci-type-choice ref="ciTypeChoice" @getCiTypeAttr="showCiType" />
</div>
<a-row>
<a-col :span="12">
<upload-file-form :ciType="ciType" ref="uploadFileForm" @uploadDone="uploadDone"></upload-file-form>
<upload-file-form
:isUploading="isUploading"
:ciType="ciType"
ref="uploadFileForm"
@uploadDone="uploadDone"
></upload-file-form>
</a-col>
<a-col :span="24" v-if="ciType && uploadData.length">
<CiUploadTable :ciTypeAttrs="ciTypeAttrs" ref="ciUploadTable" :uploadData="uploadData"></CiUploadTable>
@@ -13,15 +18,19 @@
<a-space size="large">
<a-button type="primary" ghost @click="handleCancel">取消</a-button>
<a-button @click="handleUpload" type="primary">上传</a-button>
<a-button v-if="hasError && !isUploading" @click="downloadError" type="primary">失败下载</a-button>
</a-space>
</div>
</a-col>
<a-col :span="24">
<a-col :span="24" v-if="ciType">
<upload-result
ref="uploadResult"
:upLoadData="uploadData"
:ciType="ciType"
:unique-field="uniqueField"
:isUploading="isUploading"
@uploadResultDone="uploadResultDone"
@uploadResultError="uploadResultError"
></upload-result>
</a-col>
</a-row>
@@ -29,6 +38,7 @@
</template>
<script>
import moment from 'moment'
import { mapState } from 'vuex'
import CiTypeChoice from './modules/CiTypeChoice'
import CiUploadTable from './modules/CiUploadTable'
@@ -51,7 +61,8 @@ export default {
ciType: 0,
uniqueField: '',
uniqueId: 0,
displayUpload: true,
isUploading: false,
hasError: false,
}
},
computed: {
@@ -59,13 +70,12 @@ export default {
windowHeight: (state) => state.windowHeight,
}),
},
inject: ['reload'],
methods: {
showCiType(message) {
this.ciTypeAttrs = message
this.ciType = message.type_id
this.uniqueField = message.unique
this.uniqueId = message.unique_id
this.ciTypeAttrs = message ?? {}
this.ciType = message?.type_id ?? 0
this.uniqueField = message?.unique ?? ''
this.uniqueId = message?.unique_id ?? 0
},
uploadDone(dataList) {
const _uploadData = filterNull(dataList).map((item, i) => {
@@ -73,7 +83,20 @@ export default {
const _ele = {}
item.forEach((ele, j) => {
if (ele !== undefined && ele !== null) {
_ele[dataList[0][j]] = ele
const _find = this.ciTypeAttrs.attributes.find(
(attr) => attr.alias === dataList[0][j] || attr.name === dataList[0][j]
)
if (_find?.value_type === '4' && typeof ele === 'number') {
_ele[dataList[0][j]] = moment(Math.round((ele - 25569) * 86400 * 1000 - 28800000)).format('YYYY-MM-DD')
} else if (_find?.value_type === '3' && typeof ele === 'number') {
_ele[dataList[0][j]] = moment(Math.round((ele - 25569) * 86400 * 1000 - 28800000)).format(
'YYYY-MM-DD HH:mm:ss'
)
} else if (_find?.value_type === '5' && typeof ele === 'number') {
_ele[dataList[0][j]] = moment(Math.round(ele * 86400 * 1000 - 28800000)).format('HH:mm:ss')
} else {
_ele[dataList[0][j]] = ele
}
}
})
return _ele
@@ -81,6 +104,8 @@ export default {
return item
})
this.uploadData = _uploadData.slice(1)
this.hasError = false
this.isUploading = false
},
handleUpload() {
if (!this.ciType) {
@@ -88,6 +113,7 @@ export default {
return
}
if (this.uploadData && this.uploadData.length > 0) {
this.isUploading = true
this.$nextTick(() => {
this.$refs.uploadResult.upload2Server()
})
@@ -96,7 +122,24 @@ export default {
}
},
handleCancel() {
this.reload()
if (!this.isUploading) {
this.showCiType(null)
this.$refs.ciTypeChoice.selectNum = null
this.hasError = false
} else {
this.$message.warning('批量上传已取消')
this.isUploading = false
}
},
uploadResultDone() {
this.isUploading = false
},
uploadResultError(index) {
this.hasError = true
this.$refs.ciUploadTable.uploadResultError(index)
},
downloadError() {
this.$refs.ciUploadTable.downloadError()
},
},
}

View File

@@ -8,6 +8,7 @@
:style="{ width: '300px' }"
class="ops-select"
:filter-option="filterOption"
v-model="selectNum"
>
<a-select-option v-for="ciType in ciTypeList" :key="ciType.name" :value="ciType.id">{{
ciType.alias
@@ -88,10 +89,12 @@
<script>
import _ from 'lodash'
import { mapState } from 'vuex'
import { downloadExcel } from '../../../utils/helper'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
import { searchPermResourceByRoleId } from '@/modules/acl/api/permission'
export default {
name: 'CiTypeChoice',
@@ -99,7 +102,7 @@ export default {
return {
ciTypeList: [],
ciTypeName: '',
selectNum: 0,
selectNum: null,
selectCiTypeAttrList: [],
visible: false,
checkedAttrs: [],
@@ -111,9 +114,21 @@ export default {
canEdit: {},
}
},
created: function() {
computed: {
...mapState({
rid: (state) => state.user.rid,
}),
},
async created() {
const { resources } = await searchPermResourceByRoleId(this.rid, {
resource_type_id: 'CIType',
app_id: 'cmdb',
})
getCITypes().then((res) => {
this.ciTypeList = res.ci_types
this.ciTypeList = res.ci_types.filter((type) => {
const _findRe = resources.find((re) => re.name === type.name)
return _findRe?.permissions.includes('create') ?? false
})
})
},
watch: {
@@ -131,7 +146,6 @@ export default {
methods: {
selectCiType(el) {
// 当选择好模板类型时的回调函数
this.selectNum = el
getCITypeAttributesById(el).then((res) => {
this.$emit('getCiTypeAttr', res)
this.selectCiTypeAttrList = res
@@ -155,7 +169,6 @@ export default {
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)

View File

@@ -1,6 +1,7 @@
<template>
<div class="cmdb-batch-upload-table">
<vxe-table
ref="xTable"
stripe
show-header-overflow
show-overflow=""
@@ -8,6 +9,8 @@
class="ops-stripe-table"
:max-height="200"
:data="dataSource"
resizable
:row-style="rowStyle"
>
<vxe-column type="seq" width="40" />
<vxe-column
@@ -36,7 +39,9 @@ export default {
},
},
data() {
return {}
return {
errorIndexList: [],
}
},
computed: {
columns() {
@@ -64,7 +69,33 @@ export default {
return _.cloneDeep(this.uploadData)
},
},
methods: {},
watch: {
uploadData() {
this.errorIndexList = []
},
},
methods: {
uploadResultError(index) {
const _errorIndexList = _.cloneDeep(this.errorIndexList)
_errorIndexList.push(index)
this.errorIndexList = _errorIndexList
},
rowStyle({ rowIndex }) {
if (this.errorIndexList.includes(rowIndex)) {
return 'color:red;'
}
},
downloadError() {
const data = this.uploadData.filter((item, index) => this.errorIndexList.includes(index))
this.$refs.xTable.exportData({
data,
type: 'xlsx',
columnFilterMethod({ column }) {
return column.property
},
})
},
},
}
</script>
<style lang="less" scoped>

View File

@@ -7,7 +7,7 @@
accept=".xls,.xlsx"
:showUploadList="false"
:fileList="fileList"
:disabled="!ciType"
:disabled="!ciType || isUploading"
>
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" />
<p class="ant-upload-text">点击或拖拽文件至此上传</p>
@@ -29,7 +29,11 @@ export default {
ciType: {
type: Number,
default: 0,
}
},
isUploading: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -40,7 +44,20 @@ export default {
percent: 0,
}
},
watch: {
ciType: {
handler(newValue) {
if (!newValue) {
this.ciItemNum = 0
this.fileList = []
this.dataList = []
this.progressStatus = 'active'
this.percent = 0
this.$emit('uploadDone', this.dataList)
}
},
},
},
methods: {
customRequest(data) {
this.fileList = [data.file]

View File

@@ -34,6 +34,10 @@ export default {
required: true,
type: String,
},
isUploading: {
type: Boolean,
default: false,
},
},
data: function() {
return {
@@ -51,33 +55,38 @@ export default {
},
},
methods: {
async sleep(n) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, n || 5)
})
},
async upload2Server() {
this.visible = true
this.success = 0
this.errorNum = 0
this.errorItems = []
for (let i = 0; i < this.total; i++) {
// await this.sleep(20)
const item = this.upLoadData[i]
await uploadData(this.ciType, item)
.then((res) => {
console.log(res)
this.success += 1
})
.catch((err) => {
this.errorNum += 1
this.errorItems.push(((err.response || {}).data || {}).message || '请求出现错误,请稍后再试')
})
.finally(() => {
this.complete += 1
})
const floor = Math.ceil(this.total / 6)
for (let i = 0; i < floor; i++) {
if (this.isUploading) {
const itemList = this.upLoadData.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => uploadData(this.ciType, x))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r, j) => {
if (r.status === 'fulfilled') {
this.success += 1
} else {
this.errorItems.push(r?.reason?.response?.data.message ?? '请求出现错误,请稍后再试')
this.errorNum += 1
this.$emit('uploadResultError', 6 * i + j)
}
})
})
.finally(() => {
this.complete += 6
})
} else {
break
}
}
if (this.isUploading) {
this.$emit('uploadResultDone')
this.$message.success('批量上传已完成')
}
},
},

View File

@@ -108,7 +108,7 @@
<span>{{ col.title }}</span>
</span>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<template v-if="col.is_choice || col.is_password || col.is_list" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -145,6 +145,18 @@
</span>
</a-select-option>
</a-select>
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
placeholder="请选择"
v-else-if="col.is_list"
:showArrow="false"
mode="tags"
class="ci-table-edit-select"
allowClear
>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
@@ -564,7 +576,10 @@ export default {
},
async openBatchDownload() {
this.$refs.batchDownload.open({ preferenceAttrList: this.preferenceAttrList })
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList,
ciTypeName: this.$route.meta.title || this.$route.meta.name,
})
},
batchDownload({ filename, type, checkedKeys }) {
const jsonAttrList = []
@@ -654,13 +669,19 @@ export default {
let errorNum = 0
this.loading = true
this.loadTip = `正在删除...`
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteCI(this.selectedRowKeys[i], false)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => deleteCI(x, false))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = `正在删除${this.selectedRowKeys.length}成功${successNum}失败${errorNum}`
@@ -873,6 +894,10 @@ export default {
unsubscribe(ciType, type = 'all') {
const promises = [subscribeCIType(this.typeId, ''), subscribeTreeView(this.typeId, '')]
Promise.all(promises).then(() => {
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (Number(ciType) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '')
}
this.$message.success('取消订阅成功')
this.resetRoute()
this.$router.push('/cmdb/preference')

View File

@@ -276,15 +276,16 @@ export default {
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue = row[column.property]
if (cellValue && fields.includes(column.property)) {
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.property] === cellValue) {
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {

View File

@@ -59,6 +59,9 @@
{{ ci[attr.name] }}
</span>
</template>
<template v-else-if="attr.is_list">
<span> {{ ci[attr.name].join(',') }}</span>
</template>
<template v-else>{{ getName(ci[attr.name]) }}</template>
</span>
<template v-else>
@@ -75,7 +78,6 @@
placeholder="请选择"
v-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'"
:multiple="attr.is_list"
showSearch
allowClear
size="small"
@@ -103,6 +105,23 @@
</span>
</a-select-option>
</a-select>
<a-select
:style="{ width: '100%' }"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
placeholder="请选择"
v-else-if="attr.is_list"
mode="tags"
showSearch
allowClear
size="small"
:getPopupContainer="(trigger) => trigger.parentElement"
>
</a-select>
<a-input-number
size="small"
v-decorator="[
@@ -222,7 +241,7 @@ export default {
this.$nextTick(async () => {
if (this.attr.is_list && !this.attr.is_choice) {
this.form.setFieldsValue({
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
[`${this.attr.name}`]: this.ci[this.attr.name] || null,
})
return
}

View File

@@ -28,7 +28,6 @@
placeholder="请选择"
v-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'"
:multiple="attr.is_list"
showSearch
allowClear
>

View File

@@ -53,8 +53,8 @@ export default {
return postCITypeDiscovery(this.CITypeId, { adr_id: id, interval: type === 'agent' ? 300 : 3600 })
})
await Promise.all(promises)
.then(() => {
this.getCITypeDiscovery(this.selectedIds[0].id)
.then((res) => {
this.getCITypeDiscovery(res[0].id)
this.$message.success('添加成功')
})
.catch(() => {

View File

@@ -2,20 +2,22 @@
<div class="attr-ad" :style="{ height: `${windowHeight - 104}px` }">
<div v-if="adCITypeList && adCITypeList.length">
<a-tabs size="small" v-model="currentTab">
<a-tab-pane v-for="item in adCITypeList" :key="item.adr_id">
<a-tab-pane v-for="item in adCITypeList" :key="item.id">
<a-space slot="tab">
<span>{{ getADCITypeParam(item.adr_id) }}</span>
<span v-if="item.extra_option && item.extra_option.alias">{{ item.extra_option.alias }}</span>
<span v-else>{{ getADCITypeParam(item.adr_id) }}</span>
<a-icon type="close-circle" @click="(e) => deleteADT(e, item)" />
</a-space>
<AttrADTabpane
:ref="`attrAdTabpane_${item.adr_id}`"
:currentTab="item.adr_id"
:ref="`attrAdTabpane_${item.id}`"
:adr_id="item.adr_id"
:adrList="adrList"
:adCITypeList="adCITypeList"
:currentAdt="item"
:ciTypeAttributes="ciTypeAttributes"
:currentAdr="getADCITypeParam(item.adr_id, undefined, true)"
@openEditDrawer="(data, type, adType) => openEditDrawer(data, type, adType)"
@handleSave="getCITypeDiscovery"
/>
</a-tab-pane>
<a-space
@@ -134,8 +136,8 @@ export default {
async getCITypeDiscovery(currentTab) {
await getCITypeDiscovery(this.CITypeId).then((res) => {
this.adCITypeList = res.filter((item) => item.adr_id)
if (res && res.length && !this.currentTab) {
this.currentTab = res[0].adr_id
if (this.adCITypeList && this.adCITypeList.length && !this.currentTab) {
this.currentTab = this.adCITypeList[0].id
}
if (currentTab) {
this.currentTab = currentTab
@@ -156,7 +158,7 @@ export default {
e.stopPropagation()
const that = this
this.$confirm({
title: `确认删除 ${this.getADCITypeParam(item.adr_id)}`,
title: `确认删除 ${item?.extra_option?.alias || this.getADCITypeParam(item.adr_id)}`,
content: (h) => (
<div>
<a-checkbox v-model={that.deletePlugin}>删除插件</a-checkbox>
@@ -164,18 +166,22 @@ export default {
),
onOk() {
deleteCITypeDiscovery(item.id).then(async () => {
if (that.currentTab === item.adr_id) {
if (that.currentTab === item.id) {
that.currentTab = ''
}
that.deletePlugin = false
that.$message.success('删除成功!')
that.getCITypeDiscovery()
if (that.deletePlugin) {
await deleteDiscovery(item.adr_id)
await deleteDiscovery(item.adr_id).finally(() => {
that.deletePlugin = false
})
}
that.deletePlugin = false
})
},
onCancel() {},
onCancel() {
that.deletePlugin = false
},
})
},
openEditDrawer(data, type, adType) {
@@ -183,12 +189,12 @@ export default {
},
async updateNotInner(adr) {
const _idx = this.adCITypeList.findIndex((item) => item.adr_id === adr.id)
let res
if (_idx < 0) {
await postCITypeDiscovery(this.CITypeId, { adr_id: adr.id, interval: 300 })
res = await postCITypeDiscovery(this.CITypeId, { adr_id: adr.id, interval: 300 })
}
await this.getDiscovery()
await this.getCITypeDiscovery()
this.currentTab = adr.id
await this.getCITypeDiscovery(res?.id ?? undefined)
this.$nextTick(() => {
this.$refs[`attrAdTabpane_${this.currentTab}`][0].init()
})

View File

@@ -14,6 +14,7 @@
<span>编辑</span>
</a-space>
</a>
<div>别名<a-input v-model="alias" style="width:200px;" /></div>
<div class="attr-ad-header">字段映射</div>
<vxe-table
v-if="adrType === 'agent'"
@@ -56,7 +57,7 @@
:ruleName="adrName"
:ciTypeAttributes="ciTypeAttributes"
:adCITypeList="adCITypeList"
:currentTab="currentTab"
:currentTab="adr_id"
:style="{ marginBottom: '20px' }"
/>
<a-form-model
@@ -133,7 +134,7 @@ export default {
name: 'AttrADTabpane',
components: { Vcrontab, HttpSnmpAD, CMDBExprDrawer, MonitorNodeSetting },
props: {
currentTab: {
adr_id: {
type: Number,
default: 0,
},
@@ -187,6 +188,7 @@ export default {
},
],
form3: this.$form.createForm(this, { name: 'snmp_form' }),
alias: '',
}
},
computed: {
@@ -205,7 +207,7 @@ export default {
},
agentTypeRadioList() {
const { permissions = [] } = this.userRoles
if (permissions.includes('cmdb_admin') || permissions.includes('admin')) {
if ((permissions.includes('cmdb_admin') || permissions.includes('admin')) && this.adrType !== 'http') {
return [
{ value: 'all', label: '所有节点' },
{ value: 'agent_id', label: '指定节点' },
@@ -221,8 +223,9 @@ export default {
mounted() {},
methods: {
init() {
const _find = this.adrList.find((item) => Number(item.id) === Number(this.currentTab))
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
const _find = this.adrList.find((item) => Number(item.id) === Number(this.adr_id))
const _findADT = this.adCITypeList.find((item) => Number(item.id) === Number(this.currentAdt.id))
this.alias = _findADT?.extra_option?.alias ?? ''
if (this.adrType === 'http') {
const { category = undefined, key = '', secret = '' } = _findADT?.extra_option ?? {}
this.form2 = {
@@ -294,7 +297,7 @@ export default {
this.cron = cron
},
handleSave() {
const { currentAdt } = this
const { currentAdt, alias } = this
let params
if (this.adrType === 'http') {
params = {
@@ -360,9 +363,15 @@ export default {
return
}
}
if (params.extra_option) {
params.extra_option.alias = alias
} else {
params.extra_option = {}
params.extra_option.alias = alias
}
putCITypeDiscovery(currentAdt.id, params).then((res) => {
this.$message.success('保存成功')
this.$emit('handleSave')
})
},
handleOpenCmdb() {

View File

@@ -104,6 +104,10 @@ export default {
type: Boolean,
default: false,
},
attributes: {
type: Array,
default: () => []
}
},
data() {
const propertyList = [
@@ -160,7 +164,7 @@ export default {
})
},
openTrigger() {
this.$refs.triggerForm.open(this.property)
this.$refs.triggerForm.open(this.property, this.attributes)
},
handleCalcComputed() {
const that = this

View File

@@ -98,6 +98,7 @@
:property="item"
@ok="handleOk"
:CITypeId="CITypeId"
:attributes="attributes"
/>
<i></i> <i></i> <i></i> <i></i> <i></i>
</draggable>
@@ -137,6 +138,7 @@
:property="item"
@ok="handleOk"
:CITypeId="CITypeId"
:attributes="attributes"
/>
<i></i> <i></i> <i></i> <i></i> <i></i>
</draggable>

View File

@@ -40,20 +40,18 @@
<a-menu-item key="0">
<a-upload
name="file"
accept="json"
accept=".json"
:showUploadList="false"
style="display: inline-block"
action="/api/v0.1/ci_types/template/import/file "
action="/api/v0.1/ci_types/template/import/file"
@change="changeUploadFile"
>
<a-space
><a><a-icon type="upload"/></a><a>导入</a></a-space
>
<a><a-icon type="upload"/></a><a> 导入</a>
</a-upload>
</a-menu-item>
<a-menu-item key="1">
<a-space>
<a><a-icon type="download"/></a>
<a href="/api/v0.1/ci_types/template/export/file">导出</a>
<a href="/api/v0.1/ci_types/template/export/file"><a-icon type="download" /> 导出</a>
</a-space>
</a-menu-item>
</a-menu>
@@ -134,6 +132,12 @@
<a-space class="ci-types-left-detail-action">
<a><a-icon type="user-add" @click="(e) => handlePerm(e, ci)"/></a>
<a><a-icon type="edit" @click="(e) => handleEdit(e, ci)"/></a>
<a
v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"
@click="(e) => handleDownloadCiType(e, ci)"
><a-icon
type="download"
/></a>
<a style="color: red" @click="(e) => handleDelete(e, ci)"><a-icon type="delete"/></a>
</a-space>
</div>
@@ -293,6 +297,7 @@ import SplitPane from '@/components/SplitPane'
import CMDBGrant from '../../components/cmdbGrant'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import AttributeStore from './attributeStore.vue'
import { getAllDepAndEmployee } from '@/api/company'
export default {
name: 'CITypes',
@@ -308,6 +313,7 @@ export default {
OpsMoveIcon,
AttributeStore,
},
inject: ['reload'],
data() {
return {
emptyImage,
@@ -342,6 +348,8 @@ export default {
orderSelectionOptions: [],
default_order_asc: '1',
allTreeDepAndEmp: [],
}
},
computed: {
@@ -405,9 +413,13 @@ export default {
resource_type: () => {
return this.resource_type
},
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
const _currentId = localStorage.getItem('ops_cityps_currentId')
if (_currentId) {
this.currentId = _currentId
@@ -419,6 +431,11 @@ export default {
this.getAttributes()
},
methods: {
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
async loadCITypes(isResetCurrentId = false) {
const groups = await getCITypeGroupsConfig({ need_other: true })
let alreadyReset = false
@@ -704,6 +721,21 @@ export default {
},
})
},
handleDownloadCiType(e, ci) {
e.preventDefault()
e.stopPropagation()
const x = new XMLHttpRequest()
x.open('GET', `/api/v0.1/ci_types/${ci.id}/template/export`, true)
x.responseType = 'blob'
x.onload = function(e) {
const url = window.URL.createObjectURL(x.response)
const a = document.createElement('a')
a.href = url
a.download = `${ci.alias || ci.name}.json`
a.click()
}
x.send()
},
resetRoute() {
resetRouter()
const roles = store.getters.roles
@@ -768,6 +800,19 @@ export default {
}
})
},
changeUploadFile({ file, fileList, event }) {
const key = 'upload'
if (file.status === 'uploading') {
this.$message.loading({ content: '正在导入中', key, duration: 0 })
}
if (file.status === 'done') {
this.$message.success({ content: '导入成功', key, duration: 2 })
this.reload()
}
if (file.status === 'error') {
this.$message.error({ content: '导入失败,请稍后重试', key, duration: 2 })
}
},
},
}
</script>

View File

@@ -372,7 +372,7 @@ export default {
},
async open(property, attrList) {
this.visible = true
this.getNoticeConfigAppBot()
await this.getNoticeConfigAppBot()
this.attrList = attrList
if (property.has_trigger) {
this.triggerId = property.trigger.id

View File

@@ -71,7 +71,6 @@ import _ from 'lodash'
import { getTriggerList, deleteTrigger, updateTrigger } from '../../api/CIType'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
import TriggerForm from './triggerForm.vue'
import { getAllDepAndEmployee } from '@/api/company'
export default {
name: 'TriggerTable',
@@ -86,7 +85,6 @@ export default {
return {
tableData: [],
attrList: [],
allTreeDepAndEmp: [],
}
},
computed: {
@@ -97,20 +95,9 @@ export default {
provide() {
return {
refresh: this.getTableData,
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
},
methods: {
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
async getTableData() {
const [triggerList, attrList] = await Promise.all([
getTriggerList(this.CITypeId),

View File

@@ -61,7 +61,7 @@
</vxe-column>
<vxe-column field="type_id" title="模型" width="150px">
<template #default="{ row }">
{{ row.operate_type === '删除模型' ? row.change.alias : row.type_id}}
{{ row.operate_type === '删除模型' ? row.change.alias : row.type_id }}
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">

View File

@@ -314,6 +314,12 @@ export default {
}
Promise.all(promises).then(() => {
if (type === 'all' || type === 'ci') {
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (Number(ciType.id) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '')
}
}
that.$message.success('取消订阅成功')
that.resetRoute()
})

View File

@@ -25,7 +25,7 @@
:expandedKeys="expandedKeys"
>
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title,isLeaf }">
<template #title="{ key: treeKey, title, isLeaf }">
<ContextMenu
:title="title"
:treeKey="treeKey"
@@ -135,7 +135,7 @@
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<template v-if="col.is_choice || col.is_password || col.is_list" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -172,6 +172,18 @@
</span>
</a-select-option>
</a-select>
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
placeholder="请选择"
v-else-if="col.is_list"
:showArrow="false"
mode="tags"
class="ci-table-edit-select"
allowClear
>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
@@ -310,6 +322,7 @@
v-else-if="relationViews.name2id && !relationViews.name2id.length"
></a-alert>
<AddTableModal ref="addTableModal" @reload="reload" />
<!-- <GrantDrawer ref="grantDrawer" resourceTypeName="RelationView" app_id="cmdb" /> -->
<CMDBGrant ref="cmdbGrant" resourceType="RelationView" app_id="cmdb" />
<ci-detail ref="detail" :typeId="Number(currentTypeId[0])" />
@@ -366,6 +379,7 @@ export default {
SearchForm,
AddTableModal,
ContextMenu,
// GrantDrawer,
CMDBGrant,
SplitPane,
ElTree: Tree,
@@ -483,11 +497,11 @@ export default {
},
inject: ['reload'],
watch: {
'$route.path': function(newPath, oldPath) {
'$route.path': function (newPath, oldPath) {
this.viewId = this.$route.params.viewId
this.reload()
},
pageNo: function(newPage, oldPage) {
pageNo: function (newPage, oldPage) {
this.loadData({ pageNo: newPage }, undefined, this.sortByTable)
},
},
@@ -639,6 +653,9 @@ export default {
} else {
q = `q=_type:${this.currentTypeId[0]},` + q
}
if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1`
}
if (this.currentTypeId[0]) {
const res = await searchCIRelation(q)
@@ -677,6 +694,7 @@ export default {
root_ids: key.split('%')[0],
level: this.treeKeys.length - index,
type_ids: this.showTypes.map((type) => type.id).join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')),
}).then((res) => {
let result
const getTreeItem = (data, id) => {
@@ -730,7 +748,10 @@ export default {
_showTypes = JSON.parse(JSON.stringify(this.origShowTypes))
}
const promises = _showTypeIds.map((typeId) => {
const _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
let _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
if (Object.values(this.level2constraint).includes('2')) {
_q = _q + `&&has_m2m=1`
}
console.log(_q)
if (this.treeKeys.length === 0) {
return searchCI2(_q).then((res) => {
@@ -786,6 +807,7 @@ export default {
root_ids: ciIds.join(','),
level: level,
type_ids: this.showTypes.map((type) => type.id).join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')),
}).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
@@ -815,6 +837,9 @@ export default {
.map((item) => item.split('%')[0])
.join(',')}`
}
if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1`
}
searchCIRelation(q).then(async (res) => {
const facet = []
const ciIds = []
@@ -837,6 +862,7 @@ export default {
root_ids: ciIds.join(','),
level: _level - 1,
type_ids: this.showTypes.map((type) => type.id).join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')),
}).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
@@ -1310,7 +1336,10 @@ export default {
})
},
async openBatchDownload() {
this.$refs.batchDownload.open({ preferenceAttrList: this.preferenceAttrList })
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList,
ciTypeName: this.$route.meta.name,
})
},
batchDownload({ filename, type, checkedKeys }) {
const jsonAttrList = []

View File

@@ -130,22 +130,27 @@ export default {
exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}
const res = await searchCI({
await searchCI({
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: 50,
page: this.currentPage,
sort,
})
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
.then((res) => {
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
})
.catch(() => {
this.loading = false
})
},
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal')

View File

@@ -193,7 +193,7 @@
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<template v-if="col.is_choice || col.is_password || col.is_list" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -230,6 +230,18 @@
</span>
</a-select-option>
</a-select>
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
placeholder="请选择"
v-else-if="col.is_list"
:showArrow="false"
mode="tags"
class="ci-table-edit-select"
allowClear
>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
@@ -504,7 +516,7 @@ export default {
},
},
watch: {
'$route.path': function(newPath, oldPath) {
'$route.path': function (newPath, oldPath) {
this.newLoad = true
this.typeId = this.$route.params.typeId
this.initPage()
@@ -972,7 +984,10 @@ export default {
const $table = this.$refs['xTable'].getVxetableRef()
const data = {}
this.columns.forEach((item) => {
if (!(item.field in this.initialPasswordValue) && !_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
if (
!(item.field in this.initialPasswordValue) &&
!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])
) {
data[item.field] = row[item.field] ?? null
}
})
@@ -1039,7 +1054,10 @@ export default {
this.$refs.jsonEditor.open(column, row)
},
async openBatchDownload() {
this.$refs.batchDownload.open({ preferenceAttrList: this.currentAttrList })
this.$refs.batchDownload.open({
preferenceAttrList: this.currentAttrList,
ciTypeName: this.$route.meta.title || this.$route.meta.name,
})
},
batchDownload({ filename, type, checkedKeys }) {
console.log(filename, type)
@@ -1084,13 +1102,19 @@ export default {
let errorNum = 0
this.loading = true
this.loadTip = `正在删除...`
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteCI(this.selectedRowKeys[i], false)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => deleteCI(x, false))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = `正在删除${this.selectedRowKeys.length}成功${successNum}失败${errorNum}`

View File

@@ -92,7 +92,13 @@ export const generatorDynamicRouter = async () => {
meta: { title: '飞书', icon: 'ops-setting-notice-feishu', selectedIcon: 'ops-setting-notice-feishu-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/feishu')
}]
}
},
{
path: '/setting/auth',
name: 'company_auth',
meta: { title: '认证设置', appName: 'backend', icon: 'ops-setting-auth', selectedIcon: 'ops-setting-auth-selected', permission: ['acl_admin'] },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/auth/index')
},
]
},])
return routes
@@ -112,6 +118,11 @@ export const constantRouterMap = [
name: 'login',
component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login'),
},
{
path: '/user/logout',
name: 'logout',
component: () => import(/* webpackChunkName: "user" */ '@/views/user/Logout'),
},
{
path: '/user',
component: UserLayout,

View File

@@ -1,4 +1,6 @@
import { generatorDynamicRouter, constantRouterMap } from '@/router/config'
import { searchPermResourceByRoleId } from '@/modules/acl/api/permission'
import store from '@/store'
import user from './user'
/**
* 过滤账户是否拥有某一个权限,并将菜单从加载列表移除
@@ -7,37 +9,47 @@ import user from './user'
* @param route
* @returns {boolean}
*/
function hasPermission(permission, route) {
const { detailPermissions } = user.state
if (route.meta && route.meta.permission) {
// let flag = false
// for (let i = 0, len = permission.length; i < len; i++) {
// flag = (route.meta.permission || []).includes(permission[i])
// if (flag) {
// return true
// }
// }
// return false
const totalPer = [...route.meta.appName && detailPermissions[`${route.meta.appName}`] ? detailPermissions[`${route.meta.appName}`].map(item => item.name) : [], ...permission]
return route.meta.permission.some(item => totalPer.includes(item))
}
return true
async function hasPermission(permission, route) {
return new Promise(async (resolve, reject) => {
const { detailPermissions } = user.state
if (route.meta && route.meta.permission) {
const totalPer = [...route.meta.appName && detailPermissions[`${route.meta.appName}`] ? detailPermissions[`${route.meta.appName}`].map(item => item.name) : [], ...permission]
let flag = false
if (route.name === 'ci_type') {
await searchPermResourceByRoleId(store.state.user.rid, {
resource_type_id: 'page',
app_id: 'cmdb',
}).then(res => {
const { resources } = res
const _idx = resources.findIndex(item => item.name === '模型配置')
flag = flag || (_idx > -1)
})
}
resolve(route.meta.permission.some(item => totalPer.includes(item)) || flag)
}
resolve(true)
})
}
function filterAsyncRouter(routerMap, roles) {
return routerMap.filter(route => {
async function filterAsyncRouter(routerMap, roles) {
const filteredRoutes = []
for (let i = 0; i < routerMap.length; i++) {
const route = routerMap[i]
const default_route = ['company_info', 'company_structure', 'company_group']
if (default_route.includes(route.name)) {
return true
filteredRoutes.push(route)
} else {
await hasPermission(roles.permissions, route).then(async flag => {
if (flag) {
if (route.children && route.children.length) {
route.children = await filterAsyncRouter(route.children, roles)
}
filteredRoutes.push(route)
}
})
}
if (hasPermission(roles.permissions, route)) {
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, roles)
}
return true
}
return false
})
}
return filteredRoutes
}
const routes = {
@@ -55,8 +67,8 @@ const routes = {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
generatorDynamicRouter().then(routers => {
const accessedRouters = filterAsyncRouter(routers, roles)
generatorDynamicRouter().then(async routers => {
const accessedRouters = await filterAsyncRouter(routers, roles)
commit('SET_ROUTERS', accessedRouters)
resolve(accessedRouters)
})

View File

@@ -6,6 +6,7 @@ import { getAllUsers } from '../../api/login'
import { searchPermResourceByRoleId } from '@/modules/acl/api/permission'
import { getEmployeeByUid, getEmployeeList } from '@/api/employee'
import { getAllDepartmentList } from '@/api/company'
import { getAuthDataEnable } from '@/api/auth'
const user = {
state: {
@@ -44,7 +45,8 @@ const user = {
nickname: '',
sex: '',
position_name: '',
direct_supervisor_id: null
direct_supervisor_id: null,
auth_enable: {}
},
mutations: {
@@ -52,13 +54,14 @@ const user = {
state.token = token
},
SET_USER_INFO: (state, { name, welcome, avatar, roles, info, uid, username, mobile, department_id, employee_id, email, nickname, sex, position_name, direct_supervisor_id, annual_leave }) => {
SET_USER_INFO: (state, { name, welcome, avatar, roles, info, uid, rid, username, mobile, department_id, employee_id, email, nickname, sex, position_name, direct_supervisor_id, annual_leave }) => {
state.name = name
state.welcome = welcome
state.avatar = avatar
state.roles = roles
state.info = info
state.uid = uid
state.rid = rid
state.authed = true
state.username = username
state.mobile = mobile
@@ -87,13 +90,27 @@ const user = {
...data
} : state.detailPermissions
},
SET_AUTH_ENABLE: (state, data) => {
state.auth_enable = data
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
// 获取enable_list
GetAuthDataEnable({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
getAuthDataEnable().then(res => {
commit('SET_AUTH_ENABLE', res)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 登录
Login({ commit }, { userInfo, auth_type = undefined }) {
return new Promise((resolve, reject) => {
login(userInfo, auth_type).then(response => {
Vue.ls.set(ACCESS_TOKEN, response.token, 7 * 24 * 60 * 60 * 1000)
commit('SET_TOKEN', response.token)
resolve()
@@ -133,6 +150,7 @@ const user = {
welcome: welcome(),
avatar: result.avatar,
uid: result.uid,
rid: result.rid,
username: result.username,
mobile: res.mobile,
department_id: res.department_id,
@@ -159,10 +177,11 @@ const user = {
Vue.ls.remove(ACCESS_TOKEN)
logout(state.token).then(() => {
window.location.reload()
resolve()
}).catch(() => {
resolve()
}).finally(() => {
window.location.href = '/user/logout'
})
})
},

View File

@@ -9,7 +9,6 @@ import logo from './global/logo'
import notice from './global/notice'
import getters from './global/getters'
import appConfig from '@/config/app'
console.log(appConfig)
Vue.use(Vuex)

View File

@@ -1,12 +1,11 @@
/* eslint-dsiable */
import Vue from 'vue'
import axios from 'axios'
import store from '@/store'
import { VueAxios } from './axios'
import config from '@/config/setting'
import message from 'ant-design-vue/es/message'
import notification from 'ant-design-vue/es/notification'
import { ACCESS_TOKEN } from '@/store/global/mutation-types'
import router from '@/router'
// 创建 axios 实例
const service = axios.create({
@@ -52,8 +51,8 @@ const err = (error) => {
}
if (error.response) {
console.log(error.config.url)
if (error.response.status === 401 && config.useSSO) {
store.dispatch('Login')
if (error.response.status === 401 && router.path === '/user/login') {
window.location.href = '/user/logout'
}
}
return Promise.reject(error)

View File

@@ -0,0 +1,111 @@
<template>
<a-form-model ref="form" :model="form" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rules">
<SpanTitle>基本</SpanTitle>
<a-form-model-item label="是否启用" prop="enable">
<a-switch
:checked="Boolean(form.enable)"
@change="
() => {
$set(form, 'enable', Number(!form.enable))
}
"
/>
</a-form-model-item>
<a-form-model-item label="服务端地址" prop="cas_server" help="不包括url path例如https://xxx.com">
<a-input v-model="form.cas_server" placeholder="请输入服务端地址" />
</a-form-model-item>
<a-form-model-item label="验证服务端地址" prop="cas_validate_server" help="不包括url path例如https://xxx.com">
<a-input v-model="form.cas_validate_server" placeholder="请输入验证服务端地址" />
</a-form-model-item>
<SpanTitle>其他</SpanTitle>
<a-form-model-item label="登录路由" prop="cas_login_route">
<a-input v-model="form.cas_login_route" placeholder="/cas/built-in/cas/login" />
</a-form-model-item>
<a-form-model-item label="注销路由" prop="cas_logout_route">
<a-input v-model="form.cas_logout_route" placeholder="/cas/built-in/cas/logout" />
</a-form-model-item>
<a-form-model-item label="验证路由" prop="cas_validate_route">
<a-input v-model="form.cas_validate_route" placeholder="/cas/built-in/cas/serviceValidate" />
</a-form-model-item>
<a-form-model-item label="重定向路由" prop="cas_after_login">
<a-input v-model="form.cas_after_login" placeholder="请输入重定向路由" />
</a-form-model-item>
<a-form-model-item label="用户属性映射" prop="cas_user_map" :wrapper-col="{ span: 15 }">
<vue-json-editor
:style="{ '--custom-height': `${200}px` }"
v-model="form.cas_user_map"
:showBtns="false"
mode="code"
lang="zh"
@json-change="onJsonChange"
@has-error="onJsonError"
/>
</a-form-model-item>
</a-form-model>
</template>
<script>
import _ from 'lodash'
import vueJsonEditor from 'vue-json-editor'
import SpanTitle from '../components/spanTitle.vue'
export default {
name: 'CAS',
components: { SpanTitle, vueJsonEditor },
data() {
const defaultForm = {
enable: 0,
cas_server: '',
cas_validate_server: '',
cas_login_route: '',
cas_logout_route: '',
cas_validate_route: '',
cas_after_login: '/',
cas_user_map: {
username: { tag: 'cas:user' },
nickname: { tag: 'cas:attribute', attrs: { name: 'displayName' } },
email: { tag: 'cas:attribute', attrs: { name: 'email' } },
mobile: { tag: 'cas:attribute', attrs: { name: 'phone' } },
avatar: { tag: 'cas:attribute', attrs: { name: 'avatar' } },
},
}
return {
defaultForm,
labelCol: { span: 3 },
wrapperCol: { span: 10 },
form: _.cloneDeep(defaultForm),
rules: {
enable: [{ required: true }],
cas_server: [{ required: true, message: '请输入服务端地址' }],
cas_login_route: [{ required: true, message: '请输入登录路由' }],
cas_logout_route: [{ required: true, message: '请输入注销路由' }],
cas_validate_route: [{ required: true, message: '请输入验证路由' }],
},
isJsonRight: true,
}
},
methods: {
setData(data) {
if (data) {
this.form = data
} else {
this.form = _.cloneDeep(this.defaultForm)
}
},
getData(callback) {
this.$refs.form.validate((valid) => {
if (valid && this.isJsonRight) {
callback(this.form)
}
})
},
onJsonChange(value) {
this.isJsonRight = true
},
onJsonError() {
this.isJsonRight = false
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,65 @@
<template>
<a-form-model ref="form" :model="form" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rules">
<SpanTitle>基本</SpanTitle>
<a-form-model-item
label="自动跳转到第三方登录页"
prop="auto_redirect"
help="如果关闭,则会弹出跳转到第三方登录页的确认,点取消按钮会进入系统内置的登录页"
>
<a-switch
:checked="Boolean(form.auto_redirect)"
@change="
() => {
$set(form, 'auto_redirect', Number(!form.auto_redirect))
}
"
/>
</a-form-model-item>
<!-- <a-form-model-item
label="API服务地址"
prop="api_host"
help="如果服务的部署没使用DNS, 如果要启用CAS、OAuth2.0、OIDC的则须填API服务地址"
>
<a-input v-model="form.api_host" placeholder="http://127.0.0.1:5000" />
</a-form-model-item> -->
</a-form-model>
</template>
<script>
import SpanTitle from '../components/spanTitle.vue'
export default {
name: 'AuthCommonConfig',
components: { SpanTitle },
data() {
return {
labelCol: { span: 5 },
wrapperCol: { span: 10 },
form: {
auto_redirect: 0,
api_host: '',
},
rules: {
auto_redirect: [{ required: true }],
},
}
},
methods: {
setData(data) {
if (data) {
this.form = data
} else {
this.form = { auto_redirect: 0 }
}
},
getData(callback) {
this.$refs.form.validate((valid) => {
if (valid) {
callback(this.form)
}
})
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,166 @@
<template>
<a-tabs type="card" class="ops-tab" v-model="activeKey" @change="changeActiveKey">
<a-tab-pane v-for="item in authList" :key="item.value">
<span slot="tab">
{{ item.label }}
<a-icon
v-if="enable_list.find((en) => en.auth_type === item.value)"
type="check-circle"
theme="filled"
style="color:#2f54eb"
/>
</span>
<div class="setting-auth">
<components :ref="item.value" :is="item.value === 'OIDC' ? 'OAUTH2' : item.value" :data_type="item.value" />
<a-row>
<a-col :offset="item.value === 'AuthCommonConfig' ? 5 : 3">
<a-space>
<a-button :loading="loading" type="primary" @click="handleSave">保存</a-button>
<template v-if="item.value === 'LDAP'">
<a-button :loading="loading" ghost type="primary" @click="handleTest('connect')">测试连接</a-button>
<a-button :loading="loading" ghost type="primary" @click="handleTest('login')">测试登录</a-button>
</template>
<a-button :loading="loading" @click="handleReset">重置</a-button>
</a-space>
</a-col>
</a-row>
</div>
<LoginModal v-if="item.value === 'LDAP'" ref="loginModal" @handleOK="(values) => handleTest('login', values)" />
</a-tab-pane>
</a-tabs>
</template>
<script>
import _ from 'lodash'
import LDAP from './ldap.vue'
import CAS from './cas.vue'
import AuthCommonConfig from './common.vue'
import OAUTH2 from './oauth2.vue'
import LoginModal from './loginModal.vue'
import { getAuthData, postAuthData, putAuthData, getAuthDataEnable, testLDAP } from '@/api/auth'
export default {
name: 'Auth',
components: { LDAP, CAS, AuthCommonConfig, OAUTH2, LoginModal },
data() {
const authList = [
{
value: 'LDAP',
label: 'LDAP',
},
{
value: 'CAS',
label: 'CAS',
},
{
value: 'OAUTH2',
label: 'OAUTH2',
},
{
value: 'OIDC',
label: 'OIDC',
},
{
value: 'AuthCommonConfig',
label: '通用',
},
]
return {
authList,
activeKey: 'LDAP',
dataTypeId: null,
loading: false,
enable_list: [],
}
},
mounted() {
this.changeActiveKey()
this.getAuthDataEnable()
},
methods: {
getAuthDataEnable() {
getAuthDataEnable().then((res) => {
this.enable_list = res.enable_list
})
},
changeActiveKey() {
getAuthData(this.activeKey).then((res) => {
const _res = _.cloneDeep(res)
this.$refs[this.activeKey][0].setData(_res?.data ?? null)
if (_res && JSON.stringify(_res) !== '{}') {
this.dataTypeId = _res.id
} else {
this.dataTypeId = null
}
})
},
handleSave() {
this.$refs[this.activeKey][0].getData(async (data) => {
this.loading = true
if (this.dataTypeId) {
await putAuthData(this.activeKey, this.dataTypeId, { data }).finally(() => {
this.loading = false
})
} else {
await postAuthData(this.activeKey, { data }).finally(() => {
this.loading = false
})
}
this.$message.success('保存成功')
this.changeActiveKey()
this.getAuthDataEnable()
})
},
handleReset() {
this.changeActiveKey()
},
handleTest(type, values = null) {
this.$refs[this.activeKey][0].getData(async (data) => {
if (type === 'login' && !values) {
this.$refs.loginModal[0].open()
} else {
this.loading = true
let _data = _.cloneDeep(data)
if (values) {
_data = {
..._data,
...values,
}
}
testLDAP(type, { data: _data })
.then((res) => {
this.$message.success('测试成功')
})
.finally(() => {
this.loading = false
})
}
})
},
},
}
</script>
<style lang="less" scoped>
.setting-auth {
background-color: #fff;
height: calc(100vh - 128px);
overflow: auto;
border-radius: 0 5px 5px 5px;
padding-top: 24px;
}
</style>
<style lang="less">
.setting-auth {
.jsoneditor-outer {
height: var(--custom-height) !important;
border: 1px solid #2f54eb;
}
div.jsoneditor-menu {
background-color: #2f54eb;
}
.jsoneditor-modes {
display: none;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<a-form-model ref="form" :model="form" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rules">
<SpanTitle>基本</SpanTitle>
<a-form-model-item label="是否启用" prop="enable">
<a-switch
:checked="Boolean(form.enable)"
@change="
() => {
$set(form, 'enable', Number(!form.enable))
}
"
/>
</a-form-model-item>
<a-form-model-item
label="服务器地址"
prop="ldap_server"
help="例如: 192.168.1.6 或者 ldap://192.168.1.6 或者 ldap://192.168.1.6:389"
>
<a-input v-model="form.ldap_server" placeholder="请输入服务器地址" />
</a-form-model-item>
<a-form-model-item label="" prop="ldap_domain">
<a-input v-model="form.ldap_domain" placeholder="请输入域" />
</a-form-model-item>
<SpanTitle>用户</SpanTitle>
<a-form-model-item
label="用户名称"
prop="ldap_user_dn"
help="用户dn: cn={},ou=users,dc=xxx,dc=com {}会替换成用户名"
>
<a-input v-model="form.ldap_user_dn" placeholder="请输入用户名称" />
</a-form-model-item>
</a-form-model>
</template>
<script>
import SpanTitle from '../components/spanTitle.vue'
export default {
name: 'LDAP',
components: { SpanTitle },
data() {
return {
labelCol: { span: 3 },
wrapperCol: { span: 10 },
form: {
enable: 0,
ldap_server: '',
ldap_domain: '',
ldap_user_dn: 'cn={},ou=users,dc=xxx,dc=com',
},
rules: {
enable: [{ required: true }],
ldap_server: [{ required: true, message: '请输入服务器地址' }],
},
}
},
methods: {
setData(data) {
if (data) {
this.form = { ...data }
} else {
this.form = {
enable: 0,
ldap_server: '',
ldap_domain: '',
ldap_user_dn: 'cn={},ou=users,dc=xxx,dc=com',
}
}
},
getData(callback) {
this.$refs.form.validate((valid) => {
if (valid) {
callback(this.form)
}
})
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,56 @@
<template>
<a-modal :visible="visible" @cancel="handleCancel" @ok="handleOK">
<a-form :form="form" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="用户名/邮箱">
<a-input
v-decorator="[
'username',
{
rules: [{ required: true, message: '请输入用户名或邮箱' }],
validateTrigger: 'change',
},
]"
>
</a-input>
</a-form-item>
<a-form-item label="密码">
<a-input
type="password"
autocomplete="false"
v-decorator="['password', { rules: [{ required: true, message: '请输入密码' }], validateTrigger: 'blur' }]"
>
</a-input>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
export default {
name: 'LoginModal',
data() {
return {
visible: false,
form: this.$form.createForm(this),
}
},
methods: {
open() {
this.visible = true
},
handleCancel() {
this.visible = false
},
handleOK() {
this.form.validateFields((err, values) => {
if (!err) {
this.$emit('handleOK', values)
this.handleCancel()
}
})
},
},
}
</script>
<style></style>

Some files were not shown because too many files have changed in this diff Show More