diff --git a/cmdb-api/api/commands/click_common_setting.py b/cmdb-api/api/commands/click_common_setting.py index 7243345..ddd370b 100644 --- a/cmdb-api/api/commands/click_common_setting.py +++ b/cmdb-api/api/commands/click_common_setting.py @@ -306,3 +306,13 @@ def common_check_new_columns(): 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) diff --git a/cmdb-api/api/lib/common_setting/common_data.py b/cmdb-api/api/lib/common_setting/common_data.py index 00c7398..35f96d3 100644 --- a/cmdb-api/api/lib/common_setting/common_data.py +++ b/cmdb-api/api/lib/common_setting/common_data.py @@ -1,14 +1,24 @@ -from flask import abort +import copy +import json + +from flask import abort, current_app +from ldap3 import Connection +from ldap3 import Server +from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError +from ldap3 import AUTO_BIND_NO_TLS from api.extensions import db from api.lib.common_setting.resp_format import ErrFormat from api.models.common_setting import CommonData +from api.lib.utils import AESCrypto +from api.lib.common_setting.const import AuthCommonConfig, AuthenticateType, AuthCommonConfigAutoRedirect class CommonDataCRUD(object): @staticmethod def get_data_by_type(data_type): + CommonDataCRUD.check_auth_type(data_type) return CommonData.get_by(data_type=data_type) @staticmethod @@ -18,6 +28,7 @@ class CommonDataCRUD(object): @staticmethod def create_new_data(data_type, **kwargs): try: + CommonDataCRUD.check_auth_type(data_type) return CommonData.create(data_type=data_type, **kwargs) except Exception as e: db.session.rollback() @@ -29,6 +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) return existed.update(**kwargs) except Exception as e: db.session.rollback() @@ -40,7 +52,216 @@ 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 not 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() + 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: + 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, 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}')(data) + except Exception as e: + abort(400, str(e)) + abort(400, ErrFormat.not_support_test.format(self._type)) + + @staticmethod + def test_ldap(data): + ldap_server = data.get('ldap_server') + ldap_user_dn = data.get('ldap_user_dn', '{}') + username = data.get('username', '') + user = ldap_user_dn.format(username) + password = data.get('password', '') + + server = Server(ldap_server, connect_timeout=2) + + 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 diff --git a/cmdb-api/api/lib/common_setting/const.py b/cmdb-api/api/lib/common_setting/const.py index bb585d6..bcd7b4d 100644 --- a/cmdb-api/api/lib/common_setting/const.py +++ b/cmdb-api/api/lib/common_setting/const.py @@ -19,3 +19,14 @@ BotNameMap = { 'feishuApp': 'feishuBot', 'dingdingApp': 'dingdingBot', } + + +class AuthenticateType(BaseEnum): + CAS = 'CAS' + OAUTH2 = 'OAUTH2' + OIDC = 'OIDC' + LDAP = 'LDAP' + + +AuthCommonConfig = 'AuthCommonConfig' +AuthCommonConfigAutoRedirect = 'auto_redirect' diff --git a/cmdb-api/api/lib/common_setting/resp_format.py b/cmdb-api/api/lib/common_setting/resp_format.py index 982a2a7..a5973ee 100644 --- a/cmdb-api/api/lib/common_setting/resp_format.py +++ b/cmdb-api/api/lib/common_setting/resp_format.py @@ -66,3 +66,10 @@ 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_test_unknown_error = "LDAP测试未知错误: {}" + common_data_not_support_auth_type = "通用数据不支持auth类型: {}" + diff --git a/cmdb-api/api/views/common_setting/auth_config.py b/cmdb-api/api/views/common_setting/auth_config.py new file mode 100644 index 0000000..670f970 --- /dev/null +++ b/cmdb-api/api/views/common_setting/auth_config.py @@ -0,0 +1,85 @@ +from flask import abort, request + +from api.lib.perm.acl.acl import role_required +from api.resource import APIView +from api.lib.common_setting.common_data import AuthenticateDataCRUD, CommonDataCRUD +from api.lib.common_setting.resp_format import ErrFormat + +prefix = '/auth_config' + + +class AuthConfigView(APIView): + url_prefix = (f'{prefix}/',) + + @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 + if auth_type in cli.common_type_list: + CommonDataCRUD.create_new_data(auth_type, **params) + else: + cli.create(params.get('data', {})) + + return self.jsonify(params) + + +class AuthConfigViewWithId(APIView): + url_prefix = (f'{prefix}//',) + + @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 + if auth_type in cli.common_type_list: + res = CommonDataCRUD.update_data(_id, **params) + else: + res = cli.update(_id, params.get('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}//test',) + + def post(self, auth_type): + params = request.json + return self.jsonify(AuthenticateDataCRUD(auth_type).test(params.get('data')))