mirror of
https://github.com/veops/cmdb.git
synced 2025-09-06 05:17:03 +08:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6eb8ae1dac | ||
|
f1f86ce25a | ||
|
090007487d | ||
|
aa98a304c1 | ||
|
fe22e363b4 | ||
|
2d2fb6e1d6 | ||
|
9894c77300 | ||
|
4ea947f741 | ||
|
e5ab2c2573 | ||
|
5581fa8d0f | ||
|
76c939fe5c | ||
|
6e9ce08e2c | ||
|
092a8b9b92 | ||
|
b45fd0cbbb | ||
|
5320ecfd62 | ||
|
1d253d7ad3 | ||
|
73d53f0440 | ||
|
d4a37af183 | ||
|
e03849b054 | ||
|
faed3fe6a2 | ||
|
21c9d9accd | ||
|
a06599ce33 | ||
|
2b69217136 | ||
|
03a3b8b169 | ||
|
cd319421d5 | ||
|
c918d54ea5 | ||
|
cf0ad7bad6 | ||
|
e0c8263542 | ||
|
275e8b15f3 | ||
|
6ff942c107 | ||
|
d3c87ee500 | ||
|
a4f65e7fc6 | ||
|
10527bf9b8 | ||
|
0414121c27 | ||
|
edde467c87 | ||
|
d525e1ec54 | ||
|
91c49b690f |
@@ -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
|
||||
|
@@ -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']))
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -514,9 +514,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)
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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'
|
||||
|
@@ -563,6 +563,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
|
||||
@@ -726,6 +727,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:
|
||||
|
@@ -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测试用户名必填"
|
||||
|
@@ -1,6 +1,13 @@
|
||||
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 +21,48 @@ 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):
|
||||
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 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}')
|
||||
|
@@ -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
|
||||
|
@@ -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,24 @@ 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()
|
||||
|
||||
return AuditLoginLog.create(**payload).id
|
||||
|
@@ -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 = "应用 {} 不存在!"
|
||||
|
@@ -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,11 @@ 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
|
||||
|
||||
|
@@ -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')
|
||||
|
1
cmdb-api/api/lib/perm/authentication/__init__.py
Normal file
1
cmdb-api/api/lib/perm/authentication/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
@@ -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):
|
@@ -119,4 +119,4 @@ def create_cas_validate_url(cas_url, cas_route, service, ticket,
|
||||
('service', service),
|
||||
('ticket', ticket),
|
||||
('renew', renew),
|
||||
)
|
||||
)
|
@@ -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()
|
67
cmdb-api/api/lib/perm/authentication/ldap.py
Normal file
67
cmdb-api/api/lib/perm/authentication/ldap.py
Normal 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').get('ldap_server'), get_info=ALL, connect_timeout=3)
|
||||
if '@' in username:
|
||||
email = username
|
||||
who = config['LDAP'].get('ldap_user_dn').format(username.split('@')[0])
|
||||
else:
|
||||
who = config['LDAP'].get('ldap_user_dn').format(username)
|
||||
email = "{}@{}".format(who, config['LDAP'].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['LDAP'].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)
|
30
cmdb-api/api/lib/perm/authentication/oauth2/__init__.py
Normal file
30
cmdb-api/api/lib/perm/authentication/oauth2/__init__.py
Normal 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
|
139
cmdb-api/api/lib/perm/authentication/oauth2/routing.py
Normal file
139
cmdb-api/api/lib/perm/authentication/oauth2/routing.py
Normal 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)
|
@@ -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,8 +143,6 @@ 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()
|
||||
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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()}')
|
||||
|
@@ -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('LDAP', {}).get('enabled') or config.get('LDAP', {}).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)
|
||||
|
@@ -105,6 +105,7 @@ class CITypeGroupView(APIView):
|
||||
|
||||
return self.jsonify(group.to_dict())
|
||||
|
||||
@role_required(RoleEnum.CONFIG)
|
||||
@args_validate(CITypeGroupManager.cls)
|
||||
def put(self, gid=None):
|
||||
if "/order" in request.url:
|
||||
@@ -506,3 +507,4 @@ class CITypeFilterPermissionView(APIView):
|
||||
@auth_with_app_token
|
||||
def get(self, type_id):
|
||||
return self.jsonify(CIFilterPermsCRUD().get(type_id))
|
||||
|
||||
|
88
cmdb-api/api/views/common_setting/auth_config.py
Normal file
88
cmdb-api/api/views/common_setting/auth_config.py
Normal 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')))
|
@@ -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({})
|
||||
|
@@ -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):
|
||||
@@ -53,11 +55,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))
|
||||
|
@@ -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
|
@@ -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
@@ -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
@@ -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.
Binary file not shown.
Binary file not shown.
39
cmdb-ui/src/api/auth.js
Normal file
39
cmdb-ui/src/api/auth.js
Normal 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,
|
||||
})
|
||||
}
|
@@ -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',
|
||||
|
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
BIN
cmdb-ui/src/assets/ops_logout.png
Normal file
BIN
cmdb-ui/src/assets/ops_logout.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
@@ -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('/')
|
||||
|
@@ -66,10 +66,8 @@ export default {
|
||||
|
||||
this.$confirm({
|
||||
title: '提示',
|
||||
content: '真的要注销登录吗 ?',
|
||||
content: '确认注销登录 ?',
|
||||
onOk() {
|
||||
// localStorage.removeItem('ops_cityps_currentId')
|
||||
localStorage.clear()
|
||||
return that.Logout()
|
||||
},
|
||||
onCancel() {},
|
||||
|
@@ -2,7 +2,6 @@ const appConfig = {
|
||||
buildModules: ['cmdb', 'acl'], // 需要编译的模块
|
||||
redirectTo: '/cmdb', // 首页的重定向路径
|
||||
buildAclToModules: true, // 是否在各个应用下 内联权限管理
|
||||
ssoLogoutURL: '/api/sso/logout',
|
||||
showDocs: false,
|
||||
useEncryption: false,
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
@@ -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,9 @@ export default {
|
||||
return item
|
||||
})
|
||||
this.uploadData = _uploadData.slice(1)
|
||||
this.hasError = false
|
||||
this.isUploading = false
|
||||
this.$refs.uploadResult.visible = false
|
||||
},
|
||||
handleUpload() {
|
||||
if (!this.ciType) {
|
||||
@@ -88,6 +114,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (this.uploadData && this.uploadData.length > 0) {
|
||||
this.isUploading = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.uploadResult.upload2Server()
|
||||
})
|
||||
@@ -96,7 +123,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()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -99,7 +100,7 @@ export default {
|
||||
return {
|
||||
ciTypeList: [],
|
||||
ciTypeName: '',
|
||||
selectNum: 0,
|
||||
selectNum: null,
|
||||
selectCiTypeAttrList: [],
|
||||
visible: false,
|
||||
checkedAttrs: [],
|
||||
@@ -131,7 +132,6 @@ export default {
|
||||
methods: {
|
||||
selectCiType(el) {
|
||||
// 当选择好模板类型时的回调函数
|
||||
this.selectNum = el
|
||||
getCITypeAttributesById(el).then((res) => {
|
||||
this.$emit('getCiTypeAttr', res)
|
||||
this.selectCiTypeAttrList = res
|
||||
@@ -155,7 +155,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)
|
||||
|
@@ -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>
|
||||
|
@@ -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]
|
||||
|
@@ -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('批量上传已完成')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@@ -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"
|
||||
@@ -654,13 +666,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 +891,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')
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -28,7 +28,6 @@
|
||||
placeholder="请选择"
|
||||
v-if="attr.is_choice"
|
||||
:mode="attr.is_list ? 'multiple' : 'default'"
|
||||
:multiple="attr.is_list"
|
||||
showSearch
|
||||
allowClear
|
||||
>
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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
|
||||
@@ -135,7 +137,7 @@ export default {
|
||||
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
|
||||
this.currentTab = res[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()
|
||||
})
|
||||
|
@@ -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() {
|
||||
|
@@ -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
|
||||
|
@@ -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="描述">
|
||||
|
@@ -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()
|
||||
})
|
||||
|
@@ -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"
|
||||
@@ -58,10 +58,10 @@
|
||||
/>
|
||||
<div class="relation-views-right-bar">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="isLeaf"
|
||||
type="primary"
|
||||
size="small"
|
||||
<a-button
|
||||
v-if="isLeaf"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="$refs.create.handleOpen(true, 'create')"
|
||||
>新建</a-button
|
||||
>
|
||||
@@ -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"
|
||||
@@ -483,11 +495,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)
|
||||
},
|
||||
},
|
||||
|
@@ -18,10 +18,10 @@
|
||||
属性说明
|
||||
</span>
|
||||
</span>
|
||||
<a-button
|
||||
size="small"
|
||||
icon="plus"
|
||||
type="primary"
|
||||
<a-button
|
||||
size="small"
|
||||
icon="plus"
|
||||
type="primary"
|
||||
@click="$refs.create.handleOpen(true, 'create')"
|
||||
>新建</a-button
|
||||
>
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -1084,13 +1099,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}个`
|
||||
|
@@ -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,
|
||||
|
@@ -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: {
|
||||
@@ -87,13 +89,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()
|
||||
@@ -159,10 +175,11 @@ const user = {
|
||||
Vue.ls.remove(ACCESS_TOKEN)
|
||||
|
||||
logout(state.token).then(() => {
|
||||
window.location.reload()
|
||||
resolve()
|
||||
}).catch(() => {
|
||||
resolve()
|
||||
}).finally(() => {
|
||||
window.location.href = '/user/logout'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
111
cmdb-ui/src/views/setting/auth/cas.vue
Normal file
111
cmdb-ui/src/views/setting/auth/cas.vue
Normal 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>
|
65
cmdb-ui/src/views/setting/auth/common.vue
Normal file
65
cmdb-ui/src/views/setting/auth/common.vue
Normal 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>
|
166
cmdb-ui/src/views/setting/auth/index.vue
Normal file
166
cmdb-ui/src/views/setting/auth/index.vue
Normal 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>
|
80
cmdb-ui/src/views/setting/auth/ldap.vue
Normal file
80
cmdb-ui/src/views/setting/auth/ldap.vue
Normal 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>
|
56
cmdb-ui/src/views/setting/auth/loginModal.vue
Normal file
56
cmdb-ui/src/views/setting/auth/loginModal.vue
Normal 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>
|
114
cmdb-ui/src/views/setting/auth/oauth2.vue
Normal file
114
cmdb-ui/src/views/setting/auth/oauth2.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<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="客户端ID" prop="client_id">
|
||||
<a-input v-model="form.client_id" placeholder="请输入客户端ID" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="客户端密钥" prop="client_secret">
|
||||
<a-input v-model="form.client_secret" placeholder="请输入客户端密钥" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="授权链接" prop="authorize_url">
|
||||
<a-input v-model="form.authorize_url" placeholder="请输入授权链接" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="令牌链接" prop="token_url">
|
||||
<a-input v-model="form.token_url" placeholder="请输入令牌链接" />
|
||||
</a-form-model-item>
|
||||
<SpanTitle>其他</SpanTitle>
|
||||
<a-form-model-item label="用户信息" prop="user_info" :wrapper-col="{ span: 15 }">
|
||||
<vue-json-editor
|
||||
:style="{ '--custom-height': `${200}px` }"
|
||||
v-model="form.user_info"
|
||||
:showBtns="false"
|
||||
mode="code"
|
||||
lang="zh"
|
||||
@json-change="onJsonChange"
|
||||
@has-error="onJsonError"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="范围" prop="scopes">
|
||||
<a-select mode="tags" v-model="form.scopes" placeholder="请输入范围" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="重定向路由" prop="after_login">
|
||||
<a-input v-model="form.after_login" placeholder="请输入重定向路由" />
|
||||
</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: 'OAUTH2',
|
||||
components: { SpanTitle, vueJsonEditor },
|
||||
props: {
|
||||
data_type: {
|
||||
type: String,
|
||||
default: 'OAUTH2',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const defaultForm = {
|
||||
enable: 0,
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
authorize_url: '',
|
||||
token_url: '',
|
||||
user_info: {
|
||||
url: 'https://{your-OAuth2Server-hostname}/api/userinfo',
|
||||
email: 'email',
|
||||
username: 'name',
|
||||
avatar: 'picture',
|
||||
},
|
||||
scopes: this.data_type === 'OAUTH2' ? ['profile', 'email'] : ['profile', 'email', 'openId'],
|
||||
after_login: '/',
|
||||
}
|
||||
return {
|
||||
defaultForm,
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 10 },
|
||||
form: _.cloneDeep(defaultForm),
|
||||
rules: {
|
||||
enable: [{ required: true }],
|
||||
client_id: [{ required: true, message: '请输入客户端ID' }],
|
||||
client_secret: [{ 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>
|
@@ -51,21 +51,32 @@
|
||||
class="login-button"
|
||||
:loading="state.loginBtn"
|
||||
:disabled="state.loginBtn"
|
||||
>确定</a-button
|
||||
>登录</a-button
|
||||
>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template v-if="_enable_list && _enable_list.length >= 1">
|
||||
<a-divider style="font-size:14px">其他登录方式</a-divider>
|
||||
<div style="text-align:center">
|
||||
<span v-for="(item, index) in _enable_list" :key="item.auth_type">
|
||||
<ops-icon :type="item.auth_type"/>
|
||||
<a @click="otherLogin(item.auth_type)">{{ item.auth_type }}</a>
|
||||
<a-divider v-if="index < _enable_list.length - 1" type="vertical" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import md5 from 'md5'
|
||||
import { mapActions } from 'vuex'
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import { timeFix } from '@/utils/util'
|
||||
import appConfig from '@/config/app.js'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
return {
|
||||
customActiveKey: 'tab1',
|
||||
@@ -84,9 +95,21 @@ export default {
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({ auth_enable: (state) => state?.user?.auth_enable ?? {} }),
|
||||
enable_list() {
|
||||
return this.auth_enable.enable_list ?? []
|
||||
},
|
||||
_enable_list() {
|
||||
return this.enable_list.filter((en) => en.auth_type !== 'LDAP')
|
||||
},
|
||||
},
|
||||
created() {},
|
||||
async mounted() {
|
||||
await this.GetAuthDataEnable()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['Login', 'Logout']),
|
||||
...mapActions(['Login', 'GetAuthDataEnable']),
|
||||
// handler
|
||||
handleUsernameOrEmail(rule, value, callback) {
|
||||
const { state } = this
|
||||
@@ -118,7 +141,8 @@ export default {
|
||||
delete loginParams.username
|
||||
loginParams[!state.loginType ? 'email' : 'username'] = values.username
|
||||
loginParams.password = appConfig.useEncryption ? md5(values.password) : values.password
|
||||
Login(loginParams)
|
||||
localStorage.setItem('ops_auth_type', '')
|
||||
Login({ userInfo: loginParams })
|
||||
.then((res) => this.loginSuccess(res))
|
||||
.finally(() => {
|
||||
state.loginBtn = false
|
||||
@@ -130,10 +154,11 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
otherLogin(auth_type) {
|
||||
this.Login({ userInfo: {}, auth_type })
|
||||
},
|
||||
loginSuccess(res) {
|
||||
console.log(res)
|
||||
this.$router.push({ path: this.$route.query.redirect })
|
||||
this.$router.push({ path: this.$route.query?.redirect ?? '/' })
|
||||
// 延迟 1 秒显示欢迎信息
|
||||
setTimeout(() => {
|
||||
this.$notification.success({
|
||||
|
@@ -1,23 +1,124 @@
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
<div class="ops-logout">
|
||||
<div
|
||||
class="ops-logout-box"
|
||||
v-if="_enable_list && _enable_list.length === 1 && time && !loading && !auth_auto_redirect"
|
||||
>
|
||||
<img src="../../assets/ops_logout.png" />
|
||||
<p v-if="_enable_list && _enable_list.length">
|
||||
<strong>您即将跳转至{{ _enable_list[0].auth_type }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span style="color:#2f54eb">{{ time }}</span>
|
||||
秒后自动跳转
|
||||
</p>
|
||||
<a-space size="large">
|
||||
<a-button type="primary" @click="handleConfirm">确认</a-button>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '@/config/setting'
|
||||
import appConfig from '@/config/app'
|
||||
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
export default {
|
||||
name: 'Logout',
|
||||
data() {
|
||||
return {
|
||||
msg: '正在退出,请稍后',
|
||||
interval: null,
|
||||
time: 5,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (config.useSSO) {
|
||||
window.location.href = appConfig.ssoLogoutURL
|
||||
} else {
|
||||
computed: {
|
||||
...mapState({ auth_enable: (state) => state?.user?.auth_enable ?? {} }),
|
||||
enable_list() {
|
||||
return this.auth_enable.enable_list ?? []
|
||||
},
|
||||
_enable_list() {
|
||||
return this.enable_list.filter((en) => en.auth_type !== 'LDAP')
|
||||
},
|
||||
auth_auto_redirect() {
|
||||
return this.auth_enable.auth_auto_redirect ?? 0
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
time: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (!newValue) {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
}
|
||||
if (this._enable_list.length === 1) {
|
||||
this.Login({ userInfo: {}, auth_type: this._enable_list[0].auth_type })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = true
|
||||
await this.GetAuthDataEnable()
|
||||
this.loading = false
|
||||
if (!this._enable_list.length || this._enable_list.length > 1) {
|
||||
this.$router.push('/user/login')
|
||||
}
|
||||
if (this.auth_auto_redirect) {
|
||||
this.time = 0
|
||||
} else {
|
||||
this.time = 5
|
||||
}
|
||||
if (this.time) {
|
||||
this.interval = setInterval(() => {
|
||||
this.time--
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['Login', 'GetAuthDataEnable']),
|
||||
handleConfirm() {
|
||||
if (this._enable_list.length === 1) {
|
||||
this.Login({ userInfo: {}, auth_type: this._enable_list[0].auth_type })
|
||||
}
|
||||
},
|
||||
handleCancel() {
|
||||
this.$router.push('/user/login')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ops-logout {
|
||||
background-color: #f0f5ff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.ops-logout-box {
|
||||
width: 450px;
|
||||
height: 275px;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 30%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,7 +2,7 @@ version: '3.5'
|
||||
|
||||
services:
|
||||
cmdb-db:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:3.0
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3
|
||||
container_name: cmdb-db
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
- '23306:3306'
|
||||
|
||||
cmdb-cache:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:3.0
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:2.3
|
||||
container_name: cmdb-cache
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
- redis
|
||||
|
||||
cmdb-api:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.7
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.8
|
||||
# build:
|
||||
# context: .
|
||||
# target: cmdb-api
|
||||
@@ -48,9 +48,9 @@ services:
|
||||
/wait
|
||||
flask db-setup
|
||||
flask common-check-new-columns
|
||||
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D
|
||||
gunicorn --workers=8 autoapp:app -b 0.0.0.0:5000 -D
|
||||
|
||||
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
|
||||
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=8,2 --logfile=one_cmdb_async.log -D
|
||||
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2 -D
|
||||
|
||||
nohup flask cmdb-trigger > trigger.log 2>&1 &
|
||||
@@ -67,7 +67,7 @@ services:
|
||||
- cmdb-api
|
||||
|
||||
cmdb-ui:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.7
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.8
|
||||
# build:
|
||||
# context: .
|
||||
# target: cmdb-ui
|
||||
|
@@ -34,7 +34,7 @@ RUN pip install --no-cache-dir -r requirements.txt \
|
||||
&& cp ./settings.example.py settings.py \
|
||||
&& sed -i "s#{user}:{password}@127.0.0.1:3306/{db}#cmdb:123456@mysql:3306/cmdb#g" settings.py \
|
||||
&& sed -i "s#redis://127.0.0.1#redis://redis#g" settings.py \
|
||||
&& sed -i 's#CACHE_REDIS_HOST = "127.0.0.1"#CACHE_REDIS_HOST = "redis"#g' settings.py
|
||||
&& sed -i "s#CACHE_REDIS_HOST = '127.0.0.1'#CACHE_REDIS_HOST = 'redis'#g" settings.py
|
||||
|
||||
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
|
||||
RUN chmod +x /wait
|
||||
|
@@ -6,6 +6,11 @@
|
||||
## Install
|
||||
|
||||
- 启动 mysql 服务, redis 服务
|
||||
> mysql一定要设置sql_mode, root进入mysql执行:
|
||||
>
|
||||
> `set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';`
|
||||
>
|
||||
> `set session sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';`
|
||||
|
||||
- 创建数据库 cmdb
|
||||
- 拉取代码
|
||||
|
@@ -1,6 +1,12 @@
|
||||
### Install
|
||||
|
||||
- Start mysql, redis
|
||||
> mysql must set sql_mode, and root enters mysql to execute:
|
||||
>
|
||||
> `set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';`
|
||||
>
|
||||
> `set session sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';`
|
||||
|
||||
- Create mysql database: cmdb
|
||||
- Pull code
|
||||
|
||||
|
Reference in New Issue
Block a user