Compare commits

..

2 Commits

87 changed files with 335 additions and 4441 deletions

View File

@@ -87,7 +87,7 @@ docker-compose up -d
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
sh install.sh install
```

View File

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

View File

@@ -19,8 +19,7 @@ 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.lib.perm.authentication.cas import CAS
from api.lib.perm.authentication.oauth2 import OAuth2
from api.flask_cas import CAS
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User
@@ -97,7 +96,6 @@ 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)
@@ -194,11 +192,10 @@ def configure_logger(app):
app.logger.addHandler(handler)
log_file = app.config['LOG_PATH']
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)
file_handler = RotatingFileHandler(log_file,
maxBytes=2 ** 30,
backupCount=7)
file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL']))

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,14 @@
# -*- coding:utf-8 -*-
import datetime
import uuid
import json
import bs4
from flask import Blueprint
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 flask import current_app, session, request, url_for, redirect
from flask_login import login_user, logout_user
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
@@ -26,7 +16,6 @@ from .cas_urls import create_cas_validate_url
blueprint = Blueprint('cas', __name__)
@blueprint.route('/api/cas/login')
@blueprint.route('/api/sso/login')
def login():
"""
@@ -40,20 +29,16 @@ 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)
_service = "{}://{}{}".format(urlparse(request.referrer).scheme,
urlparse(request.referrer).netloc,
url_for('cas.login'))
_service = url_for('cas.login', _external=True, next=session["next"]) \
if session.get("next") else url_for('cas.login', _external=True)
redirect_url = create_cas_login_url(
config['cas_server'],
config['cas_login_route'],
current_app.config['CAS_SERVER'],
current_app.config['CAS_LOGIN_ROUTE'],
_service)
if 'ticket' in request.args:
@@ -62,38 +47,30 @@ def login():
if request.args.get('ticket'):
if validate(request.args['ticket']):
redirect_url = session.get("next") or config.get("cas_after_login") or "/"
redirect_url = session.get("next") or \
current_app.config.get("CAS_AFTER_LOGIN")
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(
config['cas_server'],
config['cas_login_route'],
current_app.config['CAS_SERVER'],
current_app.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']
@@ -105,14 +82,12 @@ def logout():
"next" in session and session.pop("next")
redirect_url = create_cas_logout_url(
config['cas_server'],
config['cas_logout_route'],
current_app.config['CAS_SERVER'],
current_app.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)
@@ -125,15 +100,14 @@ 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(
config['cas_validate_server'],
config['cas_validate_route'],
current_app.config['CAS_VALIDATE_SERVER'],
current_app.config['CAS_VALIDATE_ROUTE'],
url_for('cas.login', _external=True),
ticket)
@@ -141,35 +115,23 @@ def validate(ticket):
try:
response = urlopen(cas_validate_url).read()
ticket_id = _parse_tag(response, "cas:user")
strs = [s.strip() for s in ticket_id.split('|') if s.strip()]
ticketid = _parse_tag(response, "cas:user")
strs = [s.strip() for s in ticketid.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("{}: {}".format(cas_username_session_key, username))
current_app.logger.debug("valid")
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)
@@ -202,5 +164,4 @@ def _parse_tag(string, tag):
if soup.find(tag) is None:
return ''
return soup.find(tag).string.strip()

View File

@@ -3,6 +3,11 @@ 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
@@ -23,10 +28,6 @@ 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__))
@@ -250,17 +251,20 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e)
return abort(400, str(e))
@staticmethod
def _can_add(**kwargs):
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'))
if kwargs.get('adr_id'):
AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
# if not adr.is_plugin:
# other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
# if other:
# ci_type = CITypeCache.get(other.type_id)
# return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
if not adr.is_plugin:
other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
if other:
ci_type = CITypeCache.get(other.type_id)
return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)

View File

@@ -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)
attrs = [AttributeCache.get(attr.attr_id) for attr in attrs]
for attr in attrs:
value_table = TableMap(attr=attr).table
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
for item in value_table.get_by(ci_id=ci_id, to_dict=False):
item.delete(commit=False)

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
# -*- coding:utf-8 -*-
from urllib.parse import urlparse
from api.extensions import cache
from api.models.common_setting import CompanyInfo
@@ -13,7 +11,6 @@ class CompanyInfoCRUD(object):
@staticmethod
def create(**kwargs):
CompanyInfoCRUD.check_data(**kwargs)
res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info)
return res
@@ -25,26 +22,10 @@ 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::'
@@ -60,4 +41,4 @@ class CompanyInfoCache(object):
@classmethod
def refresh(cls, info):
cache.set(cls.key, info)
cache.set(cls.key, info)

View File

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

View File

@@ -563,7 +563,6 @@ 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
@@ -727,7 +726,6 @@ class CreateEmployee(object):
try:
existed = self.check_acl_user(user_data)
if not existed:
user_data['add_from'] = 'common'
return self.acl.create_user(user_data)
return existed
except Exception as e:

View File

@@ -8,9 +8,6 @@ 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 = "上级部门不能是自己"
@@ -59,7 +56,6 @@ 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"
@@ -67,11 +63,3 @@ class ErrFormat(CommonErrFormat):
notice_bind_failed = "绑定失败: {}"
notice_bind_success = "绑定成功"
notice_remove_bind_success = "解绑成功"
not_support_test = "不支持的测试类型: {}"
not_support_auth_type = "不支持的认证类型: {}"
ldap_server_connect_timeout = "LDAP服务器连接超时"
ldap_server_connect_not_available = "LDAP服务器连接不可用"
ldap_test_unknown_error = "LDAP测试未知错误: {}"
common_data_not_support_auth_type = "通用数据不支持auth类型: {}"
ldap_test_username_required = "LDAP测试用户名必填"

View File

@@ -1,13 +1,6 @@
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):
@@ -21,48 +14,3 @@ 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}')

View File

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

View File

@@ -1,19 +1,14 @@
# -*- coding:utf-8 -*-
import datetime
import itertools
import json
from enum import Enum
from typing import List
from flask import has_request_context
from flask import request
from flask import has_request_context, 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
@@ -288,27 +283,6 @@ 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,
@@ -374,24 +348,3 @@ 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

View File

@@ -4,9 +4,6 @@ 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失效(尝试一下退出重新登录)"
@@ -20,11 +17,11 @@ class ErrFormat(CommonErrFormat):
role_exists = "角色 {} 已经存在!"
global_role_not_found = "全局角色 {} 不存在!"
global_role_exists = "全局角色 {} 已经存在!"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
resource_no_permission = "您没有资源: {}{} 权限"
admin_required = "需要管理员权限"
role_required = "需要角色: {}"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
app_is_ready_existed = "应用 {} 已经存在"
app_not_found = "应用 {} 不存在!"

View File

@@ -41,7 +41,6 @@ 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']))
@@ -63,11 +62,10 @@ class UserCRUD(object):
AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
)
if add_from != 'common':
from api.lib.common_setting.employee import EmployeeCRUD
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
from api.lib.common_setting.employee import EmployeeCRUD
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
return user

View File

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

View File

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

View File

@@ -1,67 +0,0 @@
# -*- 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)

View File

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

View File

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

View File

@@ -5,18 +5,17 @@ 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):
@@ -29,26 +28,21 @@ 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:
_id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else:
AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password)
from api.tasks.acl import op_record
op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
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):
@@ -63,6 +57,38 @@ 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 + '%'),
@@ -112,7 +138,6 @@ 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):
@@ -143,6 +168,8 @@ 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()
@@ -350,16 +377,3 @@ class AuditTriggerLog(Model):
current = db.Column(db.JSON, default=dict(), comment='当前数据')
extra = db.Column(db.JSON, default=dict(), comment='权限名')
source = db.Column(db.String(16), default='', comment='来源')
class AuditLoginLog(Model2):
__tablename__ = "acl_audit_login_logs"
username = db.Column(db.String(64), index=True)
channel = db.Column(db.Enum('web', 'api'), default="web")
ip = db.Column(db.String(15))
browser = db.Column(db.String(256))
description = db.Column(db.String(128))
is_ok = db.Column(db.Boolean)
login_at = db.Column(db.DateTime)
logout_at = db.Column(db.DateTime)

View File

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

View File

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

View File

@@ -8,15 +8,11 @@ from flask import abort
from flask import current_app
from flask import request
from flask import session
from flask_login import login_user
from flask_login import logout_user
from flask_login import login_user, 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
@@ -38,10 +34,8 @@ class LoginView(APIView):
username = request.values.get("username") or request.values.get("email")
password = request.values.get("password")
_role = None
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)
if current_app.config.get('AUTH_WITH_LDAP'):
user, authenticated = User.query.authenticate_with_ldap(username, password)
else:
user, authenticated = User.query.authenticate(username, password)
if not user:
@@ -182,7 +176,4 @@ class LogoutView(APIView):
@auth_abandoned
def post(self):
logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
self.jsonify(code=200)

View File

@@ -105,7 +105,6 @@ class CITypeGroupView(APIView):
return self.jsonify(group.to_dict())
@role_required(RoleEnum.CONFIG)
@args_validate(CITypeGroupManager.cls)
def put(self, gid=None):
if "/order" in request.url:
@@ -507,4 +506,3 @@ class CITypeFilterPermissionView(APIView):
@auth_with_app_token
def get(self, type_id):
return self.jsonify(CIFilterPermsCRUD().get(type_id))

View File

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

View File

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

View File

@@ -3,10 +3,9 @@ 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, CommonFileCRUD
from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name
from api.resource import APIView
prefix = '/file'
@@ -29,8 +28,7 @@ class GetFileView(APIView):
url_prefix = (f'{prefix}/<string:_filename>',)
def get(self, _filename):
file_stream = CommonFileCRUD.get_file(_filename)
return self.send_file(file_stream, as_attachment=True, download_name=_filename)
return send_from_directory(current_app.config['UPLOAD_DIRECTORY_FULL'], _filename, as_attachment=True)
class PostFileView(APIView):
@@ -55,20 +53,11 @@ class PostFileView(APIView):
filename = file.filename
if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)):
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,
)
filename = generate_new_file_name(filename)
filename = secure_filename(filename)
file.save(os.path.join(
current_app.config['UPLOAD_DIRECTORY_FULL'], filename))
return self.jsonify(file_name=new_filename)
except Exception as e:
current_app.logger.error(e)
abort(400, ErrFormat.upload_failed.format(e))
return self.jsonify(file_name=filename)
abort(400, ErrFormat.file_type_not_allowed.format(filename))
abort(400, 'Extension not allow')

View File

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

View File

@@ -11,10 +11,10 @@ from environs import Env
env = Env()
env.read_env()
ENV = env.str('FLASK_ENV', default='production')
DEBUG = ENV == 'development'
SECRET_KEY = env.str('SECRET_KEY')
BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13)
ENV = env.str("FLASK_ENV", default="production")
DEBUG = ENV == "development"
SECRET_KEY = env.str("SECRET_KEY")
BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13)
DEBUG_TB_ENABLED = DEBUG
DEBUG_TB_INTERCEPT_REDIRECTS = False
@@ -23,7 +23,7 @@ ERROR_CODES = [400, 401, 403, 404, 405, 500, 502]
# # database
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
SQLALCHEMY_BINDS = {
'user': 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
"user": 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
}
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -32,11 +32,11 @@ SQLALCHEMY_ENGINE_OPTIONS = {
}
# # cache
CACHE_TYPE = 'redis'
CACHE_REDIS_HOST = '127.0.0.1'
CACHE_TYPE = "redis"
CACHE_REDIS_HOST = "127.0.0.1"
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = ''
CACHE_KEY_PREFIX = 'CMDB::'
CACHE_REDIS_PASSWORD = ""
CACHE_KEY_PREFIX = "CMDB::"
CACHE_DEFAULT_TIMEOUT = 3000
# # log
@@ -55,10 +55,10 @@ DEFAULT_MAIL_SENDER = ''
# # queue
CELERY = {
'broker_url': 'redis://127.0.0.1:6379/2',
'result_backend': 'redis://127.0.0.1:6379/2',
'broker_vhost': '/',
'broker_connection_retry_on_startup': True
"broker_url": 'redis://127.0.0.1:6379/2',
"result_backend": "redis://127.0.0.1:6379/2",
"broker_vhost": "/",
"broker_connection_retry_on_startup": True
}
ONCE = {
'backend': 'celery_once.backends.Redis',
@@ -67,81 +67,33 @@ ONCE = {
}
}
# =============================== Authentication ===========================================================
# # 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"
# # 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'
)
# ==========================================================================================================
# # ldap
AUTH_WITH_LDAP = False
LDAP_SERVER = ''
LDAP_DOMAIN = ''
LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com'
# # pagination
DEFAULT_PAGE_COUNT = 50
# # permission
WHITE_LIST = ['127.0.0.1']
WHITE_LIST = ["127.0.0.1"]
USE_ACL = True
# # elastic search
ES_HOST = '127.0.0.1'
USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, 'Yes', 'YES', 'yes', 'Y', 'y']
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
# # messenger
USE_MESSENGER = True

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1702544951995') format('woff2'),
url('iconfont.woff?t=1702544951995') format('woff'),
url('iconfont.ttf?t=1702544951995') format('truetype');
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1698273699449') format('truetype');
}
.iconfont {
@@ -13,274 +13,6 @@
-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";
}
@@ -289,11 +21,11 @@
content: "\e895";
}
.itsm-download-all:before {
.a-itsm-oneclickdownload:before {
content: "\e892";
}
.itsm-download-package:before {
.a-itsm-packagedownload:before {
content: "\e893";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,475 +5,6 @@
"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",
@@ -491,14 +22,14 @@
{
"icon_id": "37822199",
"name": "itsm-oneclick download",
"font_class": "itsm-download-all",
"font_class": "a-itsm-oneclickdownload",
"unicode": "e892",
"unicode_decimal": 59538
},
{
"icon_id": "37822198",
"name": "itsm-package download",
"font_class": "itsm-download-package",
"font_class": "a-itsm-packagedownload",
"unicode": "e893",
"unicode_decimal": 59539
},

Binary file not shown.

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ 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 })
@@ -15,16 +16,16 @@ const whitePath = ['/user/login', '/user/logout', '/user/register', '/api/sso/lo
// 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录
// 登录页面处理处理 是否使用单点登录
router.beforeEach(async (to, from, next) => {
router.beforeEach((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 ((auth_type || (!auth_type && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) {
store.dispatch('GetAuthDataEnable')
} else if ((config.useSSO || (!config.useSSO && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) {
store.dispatch('GetInfo').then(res => {
const roles = res.result && res.result.role
store.dispatch("loadAllUsers")
@@ -45,17 +46,10 @@ router.beforeEach(async (to, from, next) => {
}).catch((e) => {
setTimeout(() => { store.dispatch('Logout') }, 3000)
})
} else if (to.path === '/user/login' && !auth_type && store.getters.roles.length !== 0) {
} else if (to.path === '/user/login' && !config.useSSO && store.getters.roles.length !== 0) {
next({ path: '/' })
} 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 if (!config.useSSO && !Vue.ls.get(ACCESS_TOKEN) && to.path !== '/user/login') {
next({ path: '/user/login', query: { redirect: to.fullPath } })
} else {
next()
}

View File

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

View File

@@ -16,14 +16,12 @@ 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,
ci_type: ciId,
exist_policy: 'replace'
},
data,
isShowMessage: false
})
}

View File

@@ -1,16 +1,11 @@
<template>
<div class="cmdb-batch-upload" :style="{ height: `${windowHeight - 64}px` }">
<div id="title">
<ci-type-choice ref="ciTypeChoice" @getCiTypeAttr="showCiType" />
<ci-type-choice @getCiTypeAttr="showCiType" />
</div>
<a-row>
<a-col :span="12">
<upload-file-form
:isUploading="isUploading"
:ciType="ciType"
ref="uploadFileForm"
@uploadDone="uploadDone"
></upload-file-form>
<upload-file-form :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>
@@ -18,19 +13,15 @@
<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" v-if="ciType">
<a-col :span="24">
<upload-result
ref="uploadResult"
:upLoadData="uploadData"
:ciType="ciType"
:unique-field="uniqueField"
:isUploading="isUploading"
@uploadResultDone="uploadResultDone"
@uploadResultError="uploadResultError"
></upload-result>
</a-col>
</a-row>
@@ -38,7 +29,6 @@
</template>
<script>
import moment from 'moment'
import { mapState } from 'vuex'
import CiTypeChoice from './modules/CiTypeChoice'
import CiUploadTable from './modules/CiUploadTable'
@@ -61,8 +51,7 @@ export default {
ciType: 0,
uniqueField: '',
uniqueId: 0,
isUploading: false,
hasError: false,
displayUpload: true,
}
},
computed: {
@@ -70,12 +59,13 @@ export default {
windowHeight: (state) => state.windowHeight,
}),
},
inject: ['reload'],
methods: {
showCiType(message) {
this.ciTypeAttrs = message ?? {}
this.ciType = message?.type_id ?? 0
this.uniqueField = message?.unique ?? ''
this.uniqueId = message?.unique_id ?? 0
this.ciTypeAttrs = message
this.ciType = message.type_id
this.uniqueField = message.unique
this.uniqueId = message.unique_id
},
uploadDone(dataList) {
const _uploadData = filterNull(dataList).map((item, i) => {
@@ -83,20 +73,7 @@ export default {
const _ele = {}
item.forEach((ele, j) => {
if (ele !== undefined && ele !== null) {
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
}
_ele[dataList[0][j]] = ele
}
})
return _ele
@@ -104,9 +81,6 @@ export default {
return item
})
this.uploadData = _uploadData.slice(1)
this.hasError = false
this.isUploading = false
this.$refs.uploadResult.visible = false
},
handleUpload() {
if (!this.ciType) {
@@ -114,7 +88,6 @@ export default {
return
}
if (this.uploadData && this.uploadData.length > 0) {
this.isUploading = true
this.$nextTick(() => {
this.$refs.uploadResult.upload2Server()
})
@@ -123,24 +96,7 @@ export default {
}
},
handleCancel() {
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()
this.reload()
},
},
}

View File

@@ -8,7 +8,6 @@
: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
@@ -100,7 +99,7 @@ export default {
return {
ciTypeList: [],
ciTypeName: '',
selectNum: null,
selectNum: 0,
selectCiTypeAttrList: [],
visible: false,
checkedAttrs: [],
@@ -132,6 +131,7 @@ export default {
methods: {
selectCiType(el) {
// 当选择好模板类型时的回调函数
this.selectNum = el
getCITypeAttributesById(el).then((res) => {
this.$emit('getCiTypeAttr', res)
this.selectCiTypeAttrList = res
@@ -155,6 +155,7 @@ export default {
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)

View File

@@ -1,7 +1,6 @@
<template>
<div class="cmdb-batch-upload-table">
<vxe-table
ref="xTable"
stripe
show-header-overflow
show-overflow=""
@@ -9,8 +8,6 @@
class="ops-stripe-table"
:max-height="200"
:data="dataSource"
resizable
:row-style="rowStyle"
>
<vxe-column type="seq" width="40" />
<vxe-column
@@ -39,9 +36,7 @@ export default {
},
},
data() {
return {
errorIndexList: [],
}
return {}
},
computed: {
columns() {
@@ -69,33 +64,7 @@ export default {
return _.cloneDeep(this.uploadData)
},
},
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
},
})
},
},
methods: {},
}
</script>
<style lang="less" scoped>

View File

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

View File

@@ -34,10 +34,6 @@ export default {
required: true,
type: String,
},
isUploading: {
type: Boolean,
default: false,
},
},
data: function() {
return {
@@ -55,38 +51,33 @@ 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 = []
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('批量上传已完成')
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
})
}
},
},

View File

@@ -108,7 +108,7 @@
<span>{{ col.title }}</span>
</span>
</template>
<template v-if="col.is_choice || col.is_password || col.is_list" #edit="{ row }">
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -145,18 +145,6 @@
</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"
@@ -666,19 +654,13 @@ export default {
let errorNum = 0
this.loading = true
this.loadTip = `正在删除...`
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
}
})
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteCI(this.selectedRowKeys[i], false)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = `正在删除${this.selectedRowKeys.length}成功${successNum}失败${errorNum}`
@@ -891,10 +873,6 @@ export default {
unsubscribe(ciType, type = 'all') {
const promises = [subscribeCIType(this.typeId, ''), subscribeTreeView(this.typeId, '')]
Promise.all(promises).then(() => {
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (Number(ciType) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '')
}
this.$message.success('取消订阅成功')
this.resetRoute()
this.$router.push('/cmdb/preference')

View File

@@ -59,9 +59,6 @@
{{ 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>
@@ -78,6 +75,7 @@
placeholder="请选择"
v-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'"
:multiple="attr.is_list"
showSearch
allowClear
size="small"
@@ -105,23 +103,6 @@
</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="[
@@ -241,7 +222,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] || null,
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
})
return
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@
:expandedKeys="expandedKeys"
>
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title, isLeaf }">
<template #title="{ key: treeKey, title,isLeaf }">
<ContextMenu
:title="title"
:treeKey="treeKey"
@@ -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 || col.is_list" #edit="{ row }">
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -172,18 +172,6 @@
</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"
@@ -495,11 +483,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)
},
},

View File

@@ -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 || col.is_list" #edit="{ row }">
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -230,18 +230,6 @@
</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"
@@ -516,7 +504,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()
@@ -984,10 +972,7 @@ 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
}
})
@@ -1099,19 +1084,13 @@ export default {
let errorNum = 0
this.loading = true
this.loadTip = `正在删除...`
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
}
})
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteCI(this.selectedRowKeys[i], false)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = `正在删除${this.selectedRowKeys.length}成功${successNum}失败${errorNum}`

View File

@@ -92,13 +92,7 @@ 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
@@ -118,11 +112,6 @@ export const constantRouterMap = [
name: 'login',
component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login'),
},
{
path: '/user/logout',
name: 'logout',
component: () => import(/* webpackChunkName: "user" */ '@/views/user/Logout'),
},
{
path: '/user',
component: UserLayout,

View File

@@ -6,7 +6,6 @@ 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: {
@@ -45,8 +44,7 @@ const user = {
nickname: '',
sex: '',
position_name: '',
direct_supervisor_id: null,
auth_enable: {}
direct_supervisor_id: null
},
mutations: {
@@ -89,27 +87,13 @@ const user = {
...data
} : state.detailPermissions
},
SET_AUTH_ENABLE: (state, data) => {
state.auth_enable = data
}
},
actions: {
// 获取enable_list
GetAuthDataEnable({ commit }, userInfo) {
return new Promise((resolve, reject) => {
getAuthDataEnable().then(res => {
commit('SET_AUTH_ENABLE', res)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 登录
Login({ commit }, { userInfo, auth_type = undefined }) {
Login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo, auth_type).then(response => {
login(userInfo).then(response => {
Vue.ls.set(ACCESS_TOKEN, response.token, 7 * 24 * 60 * 60 * 1000)
commit('SET_TOKEN', response.token)
resolve()
@@ -175,11 +159,10 @@ const user = {
Vue.ls.remove(ACCESS_TOKEN)
logout(state.token).then(() => {
window.location.reload()
resolve()
}).catch(() => {
resolve()
}).finally(() => {
window.location.href = '/user/logout'
})
})
},

View File

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

View File

@@ -1,11 +1,12 @@
/* 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({
@@ -51,8 +52,8 @@ const err = (error) => {
}
if (error.response) {
console.log(error.config.url)
if (error.response.status === 401 && router.path === '/user/login') {
window.location.href = '/user/logout'
if (error.response.status === 401 && config.useSSO) {
store.dispatch('Login')
}
}
return Promise.reject(error)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,56 +0,0 @@
<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>

View File

@@ -1,114 +0,0 @@
<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>

View File

@@ -51,32 +51,21 @@
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 { mapState, mapActions } from 'vuex'
import { mapActions } from 'vuex'
import { timeFix } from '@/utils/util'
import appConfig from '@/config/app.js'
export default {
name: 'Login',
data() {
return {
customActiveKey: 'tab1',
@@ -95,21 +84,9 @@ 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', 'GetAuthDataEnable']),
...mapActions(['Login', 'Logout']),
// handler
handleUsernameOrEmail(rule, value, callback) {
const { state } = this
@@ -141,8 +118,7 @@ export default {
delete loginParams.username
loginParams[!state.loginType ? 'email' : 'username'] = values.username
loginParams.password = appConfig.useEncryption ? md5(values.password) : values.password
localStorage.setItem('ops_auth_type', '')
Login({ userInfo: loginParams })
Login(loginParams)
.then((res) => this.loginSuccess(res))
.finally(() => {
state.loginBtn = false
@@ -154,11 +130,10 @@ export default {
}
})
},
otherLogin(auth_type) {
this.Login({ userInfo: {}, auth_type })
},
loginSuccess(res) {
this.$router.push({ path: this.$route.query?.redirect ?? '/' })
console.log(res)
this.$router.push({ path: this.$route.query.redirect })
// 延迟 1 秒显示欢迎信息
setTimeout(() => {
this.$notification.success({

View File

@@ -1,124 +1,23 @@
<template>
<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>
<h1>{{ msg }}</h1>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import config from '@/config/setting'
import appConfig from '@/config/app'
export default {
name: 'Logout',
data() {
return {
interval: null,
time: 5,
loading: false,
msg: '正在退出,请稍后',
}
},
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
mounted() {
if (config.useSSO) {
window.location.href = appConfig.ssoLogoutURL
} 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>

View File

@@ -2,7 +2,7 @@ version: '3.5'
services:
cmdb-db:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:3.0
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:2.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:3.0
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.8
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.6
# build:
# context: .
# target: cmdb-api
@@ -48,10 +48,10 @@ services:
/wait
flask db-setup
flask common-check-new-columns
gunicorn --workers=8 autoapp:app -b 0.0.0.0:5000 -D
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -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 celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=2,5 > one_cmdb_async.log 2>&1 &
nohup celery -A celery_worker.celery worker -E -Q acl_async --concurrency=2 > one_acl_async.log 2>&1 &
nohup flask cmdb-trigger > trigger.log 2>&1 &
flask cmdb-init-cache
@@ -67,7 +67,7 @@ services:
- cmdb-api
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.8
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.6
# build:
# context: .
# target: cmdb-ui

View File

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

View File

@@ -6,11 +6,6 @@
## 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
- 拉取代码

View File

@@ -1,12 +1,6 @@
### 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

View File

@@ -21,7 +21,7 @@ check_docker_compose() {
clone_repo() {
local repo_url=$1
git clone $repo_url || {
git clone -b deploy_on_kylin_docker --single-branch $repo_url || {
echo "error: failed to clone $repo_url"
exit 1
}