mirror of https://github.com/veops/cmdb.git
feat(api): auth config api (#309)
This commit is contained in:
parent
e03849b054
commit
d4a37af183
|
@ -306,3 +306,13 @@ def common_check_new_columns():
|
||||||
def common_sync_file_to_db():
|
def common_sync_file_to_db():
|
||||||
from api.lib.common_setting.upload_file import CommonFileCRUD
|
from api.lib.common_setting.upload_file import CommonFileCRUD
|
||||||
CommonFileCRUD.sync_file_to_db()
|
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)
|
||||||
|
|
|
@ -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.extensions import db
|
||||||
from api.lib.common_setting.resp_format import ErrFormat
|
from api.lib.common_setting.resp_format import ErrFormat
|
||||||
from api.models.common_setting import CommonData
|
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):
|
class CommonDataCRUD(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_data_by_type(data_type):
|
def get_data_by_type(data_type):
|
||||||
|
CommonDataCRUD.check_auth_type(data_type)
|
||||||
return CommonData.get_by(data_type=data_type)
|
return CommonData.get_by(data_type=data_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -18,6 +28,7 @@ class CommonDataCRUD(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_new_data(data_type, **kwargs):
|
def create_new_data(data_type, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
CommonDataCRUD.check_auth_type(data_type)
|
||||||
return CommonData.create(data_type=data_type, **kwargs)
|
return CommonData.create(data_type=data_type, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
@ -29,6 +40,7 @@ class CommonDataCRUD(object):
|
||||||
if not existed:
|
if not existed:
|
||||||
abort(404, ErrFormat.common_data_not_found.format(_id))
|
abort(404, ErrFormat.common_data_not_found.format(_id))
|
||||||
try:
|
try:
|
||||||
|
CommonDataCRUD.check_auth_type(existed.data_type)
|
||||||
return existed.update(**kwargs)
|
return existed.update(**kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
@ -40,7 +52,216 @@ class CommonDataCRUD(object):
|
||||||
if not existed:
|
if not existed:
|
||||||
abort(404, ErrFormat.common_data_not_found.format(_id))
|
abort(404, ErrFormat.common_data_not_found.format(_id))
|
||||||
try:
|
try:
|
||||||
|
CommonDataCRUD.check_auth_type(existed.data_type)
|
||||||
existed.soft_delete()
|
existed.soft_delete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
abort(400, str(e))
|
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
|
||||||
|
|
|
@ -19,3 +19,14 @@ BotNameMap = {
|
||||||
'feishuApp': 'feishuBot',
|
'feishuApp': 'feishuBot',
|
||||||
'dingdingApp': 'dingdingBot',
|
'dingdingApp': 'dingdingBot',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticateType(BaseEnum):
|
||||||
|
CAS = 'CAS'
|
||||||
|
OAUTH2 = 'OAUTH2'
|
||||||
|
OIDC = 'OIDC'
|
||||||
|
LDAP = 'LDAP'
|
||||||
|
|
||||||
|
|
||||||
|
AuthCommonConfig = 'AuthCommonConfig'
|
||||||
|
AuthCommonConfigAutoRedirect = 'auto_redirect'
|
||||||
|
|
|
@ -66,3 +66,10 @@ class ErrFormat(CommonErrFormat):
|
||||||
notice_bind_failed = "绑定失败: {}"
|
notice_bind_failed = "绑定失败: {}"
|
||||||
notice_bind_success = "绑定成功"
|
notice_bind_success = "绑定成功"
|
||||||
notice_remove_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类型: {}"
|
||||||
|
|
||||||
|
|
|
@ -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}/<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
|
||||||
|
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}/<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
|
||||||
|
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}/<string:auth_type>/test',)
|
||||||
|
|
||||||
|
def post(self, auth_type):
|
||||||
|
params = request.json
|
||||||
|
return self.jsonify(AuthenticateDataCRUD(auth_type).test(params.get('data')))
|
Loading…
Reference in New Issue