diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index 500c90f8..a28be353 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -32,7 +32,7 @@ def before_request(): def register_api(view, endpoint, url, pk='listener_id', pk_type='int'): - view_func = view.as_view(endpoint) + view_func = view.as_view(endpoint, True) bp.add_url_rule(url, view_func=view_func, methods=['POST']) bp.add_url_rule(f'{url}/<{pk_type}:{pk}>', view_func=view_func, methods=['GET', 'PUT', 'PATCH', 'DELETE']) diff --git a/app/create_db.py b/app/create_db.py index e03b7aad..08bac055 100644 --- a/app/create_db.py +++ b/app/create_db.py @@ -469,30 +469,6 @@ def update_db_v_4_3_0(): print("An error occurred:", e) -def update_db_v_7_1_2(): - try: - Setting.delete().where(Setting.param == 'stats_user').execute() - Setting.delete().where(Setting.param == 'stats_password').execute() - Setting.delete().where(Setting.param == 'stats_port').execute() - Setting.delete().where(Setting.param == 'stats_page').execute() - except Exception as e: - print("An error occurred:", e) - else: - print("Updating... DB has been updated to version 7.1.2") - - -def update_db_v_7_1_2_1(): - try: - migrate( - migrator.add_column('cred', 'passphrase', CharField(null=True)) - ) - except Exception as e: - if e.args[0] == 'duplicate column name: passphrase' or str(e) == '(1060, "Duplicate column name \'passphrase\'")': - print('Updating... DB has been updated to version 7.1.2-1') - else: - print("An error occurred:", e) - - def update_db_v_7_2_0(): try: if mysql_enable: @@ -620,9 +596,26 @@ def update_db_v_8(): print("An error occurred:", e) +def update_db_v_8_0_2(): + try: + if mysql_enable: + migrate( + migrator.add_column('cred', 'shared', IntegerField(default=0)), + ) + else: + migrate( + migrator.add_column('cred', 'shared', IntegerField(constraints=[SQL('DEFAULT 0')])), + ) + except Exception as e: + if e.args[0] == 'duplicate column name: shared' or str(e) == '(1060, "Duplicate column name \'shared\'")': + print('Updating... DB has been updated to version 8.0.2') + else: + print("An error occurred:", e) + + def update_ver(): try: - Version.update(version='8.0.1').execute() + Version.update(version='8.0.2').execute() except Exception: print('Cannot update version') @@ -640,12 +633,11 @@ def update_all(): if check_ver() is None: update_db_v_3_4_5_22() update_db_v_4_3_0() - update_db_v_7_1_2() - update_db_v_7_1_2_1() update_db_v_7_2_0() update_db_v_7_2_0_1() update_db_v_7_2_3() update_db_v_7_3_1() update_db_v_7_4() update_db_v_8() + update_db_v_8_0_2() update_ver() diff --git a/app/modules/db/common.py b/app/modules/db/common.py index de97ed41..4e3cf88e 100644 --- a/app/modules/db/common.py +++ b/app/modules/db/common.py @@ -2,6 +2,8 @@ import os import sys import traceback +from app.modules.roxywi.exception import RoxywiConflictError + def out_error(error): error = str(error) @@ -11,3 +13,11 @@ def out_error(error): function_name = stk[0][2] error = f'{error} in function: {function_name} in file: {file_name}' raise Exception(f'error: {error}') + + +def not_unique_error(error): + if error.args[0] == 1062: + col = error.args[1].split('key ')[1] + else: + col = error.args[0].split(': ')[-1] + raise RoxywiConflictError(f'{col} must be uniq') diff --git a/app/modules/db/cred.py b/app/modules/db/cred.py index c3751590..6cea6bdc 100644 --- a/app/modules/db/cred.py +++ b/app/modules/db/cred.py @@ -4,8 +4,15 @@ from app.modules.roxywi.exception import RoxywiResourceNotFound def select_ssh(**kwargs): - if kwargs.get("group") and kwargs.get("cred_id"): - query = Cred.select().where((Cred.id == kwargs.get('cred_id')) & (Cred.group_id == kwargs.get('group'))) + if kwargs.get("group") and kwargs.get("cred_id") and kwargs.get("not_shared"): + query = Cred.select().where( + ((Cred.id == kwargs.get('cred_id')) & (Cred.group_id == kwargs.get('group'))) + ) + elif kwargs.get("group") and kwargs.get("cred_id"): + query = Cred.select().where( + ((Cred.id == kwargs.get('cred_id')) & (Cred.group_id == kwargs.get('group'))) | + (Cred.shared == 1) + ) elif kwargs.get("name") is not None: query = Cred.select().where(Cred.name == kwargs.get('name')) elif kwargs.get("id") is not None: @@ -13,7 +20,7 @@ def select_ssh(**kwargs): elif kwargs.get("serv") is not None: query = Cred.select().join(Server, on=(Cred.id == Server.cred_id)).where(Server.ip == kwargs.get('serv')) elif kwargs.get("group") is not None: - query = Cred.select().where(Cred.group_id == kwargs.get("group")) + query = Cred.select().where((Cred.group_id == kwargs.get("group")) | (Cred.shared == 1)) else: query = Cred.select() try: @@ -26,11 +33,11 @@ def select_ssh(**kwargs): return query_res -def insert_new_ssh(name, enable, group, username, password): +def insert_new_ssh(name, enable, group, username, password, shared): if password is None: password = 'None' try: - return Cred.insert(name=name, key_enabled=enable, group_id=group, username=username, password=password).execute() + return Cred.insert(name=name, key_enabled=enable, group_id=group, username=username, password=password, shared=shared).execute() except Exception as e: out_error(e) @@ -45,21 +52,22 @@ def delete_ssh(ssh_id): return True -def update_ssh(cred_id, name, enable, group, username, password): +def update_ssh(cred_id: int, name: str, enable: int, group: int, username: str, password: bytes, shared: int): if password is None: password = 'None' - cred_update = Cred.update(name=name, key_enabled=enable, group_id=group, username=username, password=password).where( - Cred.id == cred_id) + cred_update = Cred.update( + name=name, key_enabled=enable, group_id=group, username=username, password=password, shared=shared + ).where(Cred.id == cred_id) try: cred_update.execute() except Exception as e: out_error(e) -def update_ssh_passphrase(name: str, passphrase: str): +def update_ssh_passphrase(cred_id: int, passphrase: str): try: - Cred.update(passphrase=passphrase).where(Cred.name == name).execute() + Cred.update(passphrase=passphrase).where(Cred.id == cred_id).execute() except Exception as e: out_error(e) diff --git a/app/modules/db/db_model.py b/app/modules/db/db_model.py index b099ea88..cdb70a6d 100644 --- a/app/modules/db/db_model.py +++ b/app/modules/db/db_model.py @@ -196,6 +196,7 @@ class Cred(BaseModel): password = CharField(null=True) group_id = IntegerField(constraints=[SQL('DEFAULT 1')]) passphrase = CharField(null=True) + shared = IntegerField(constraints=[SQL('DEFAULT 0')]) class Meta: table_name = 'cred' diff --git a/app/modules/db/server.py b/app/modules/db/server.py index 0fafefa2..91f03123 100644 --- a/app/modules/db/server.py +++ b/app/modules/db/server.py @@ -1,5 +1,7 @@ +from peewee import IntegrityError + from app.modules.db.db_model import mysql_enable, connect, Server, SystemInfo -from app.modules.db.common import out_error +from app.modules.db.common import out_error, not_unique_error from app.modules.roxywi.exception import RoxywiResourceNotFound @@ -10,9 +12,10 @@ def add_server(hostname, ip, group, type_ip, enable, master, cred, port, desc, h port=port, description=desc, haproxy=haproxy, nginx=nginx, apache=apache, firewall_enable=firewall ).execute() return server_id + except IntegrityError as e: + not_unique_error(e) except Exception as e: out_error(e) - return False def delete_server(server_id): diff --git a/app/modules/roxywi/class_models.py b/app/modules/roxywi/class_models.py index 771a5ed7..49cc0cfb 100644 --- a/app/modules/roxywi/class_models.py +++ b/app/modules/roxywi/class_models.py @@ -139,6 +139,7 @@ class CredRequest(BaseModel): password: Optional[EscapedString] = None key_enabled: Optional[bool] = 1 group_id: Optional[int] = None + shared: Optional[int] = 0 class CredUploadRequest(BaseModel): diff --git a/app/modules/roxywi/common.py b/app/modules/roxywi/common.py index d1989b4f..ecc61637 100644 --- a/app/modules/roxywi/common.py +++ b/app/modules/roxywi/common.py @@ -16,7 +16,8 @@ import app.modules.db.history as history_sql import app.modules.db.ha_cluster as ha_sql import app.modules.roxy_wi_tools as roxy_wi_tools from app.modules.roxywi.class_models import ErrorResponse -from app.modules.roxywi.exception import RoxywiResourceNotFound, RoxywiGroupMismatch, RoxywiGroupNotFound +from app.modules.roxywi.exception import RoxywiResourceNotFound, RoxywiGroupMismatch, RoxywiGroupNotFound, \ + RoxywiPermissionError get_config_var = roxy_wi_tools.GetConfigVar() @@ -334,5 +335,7 @@ def handler_exceptions_for_json_data(ex: Exception, main_ex_mes: str) -> tuple[d return handle_json_exceptions(ex, 'Group not found'), 404 elif isinstance(ex, RoxywiGroupMismatch): return handle_json_exceptions(ex, 'Resource not found in group'), 404 + elif isinstance(ex, RoxywiPermissionError): + return handle_json_exceptions(ex, 'You cannot edit this resource'), 403 else: return handle_json_exceptions(ex, main_ex_mes), 500 diff --git a/app/modules/roxywi/exception.py b/app/modules/roxywi/exception.py index 2e93be20..417fea9d 100644 --- a/app/modules/roxywi/exception.py +++ b/app/modules/roxywi/exception.py @@ -31,3 +31,17 @@ class RoxywiValidationError(Exception): def __init__(self, message='Validation error'): super(RoxywiValidationError, self).__init__(message) + + +class RoxywiPermissionError(Exception): + """ This class represents an exception raised when a permission error occurs. """ + + def __init__(self, message='Forbidden'): + super(RoxywiPermissionError, self).__init__(message) + + +class RoxywiConflictError(Exception): + """ This class represents an exception raised when a conflict error occurs.""" + + def __init__(self, message='Conflict'): + super(RoxywiConflictError, self).__init__(message) diff --git a/app/modules/server/ssh.py b/app/modules/server/ssh.py index bafd9659..670c9cee 100644 --- a/app/modules/server/ssh.py +++ b/app/modules/server/ssh.py @@ -13,14 +13,13 @@ import app.modules.common.common as common from app.modules.server import ssh_connection import app.modules.roxywi.common as roxywi_common import app.modules.roxy_wi_tools as roxy_wi_tools -from app.modules.roxywi.class_models import IdResponse, IdDataResponse +from app.modules.roxywi.class_models import IdResponse, IdDataResponse, CredRequest error_mess = common.error_mess get_config = roxy_wi_tools.GetConfigVar() def return_ssh_keys_path(server_ip: str, **kwargs) -> dict: - lib_path = get_config.get_config_var('main', 'lib_path') ssh_settings = {} if kwargs.get('id'): sshs = cred_sql.select_ssh(id=kwargs.get('id')) @@ -43,10 +42,10 @@ def return_ssh_keys_path(server_ip: str, **kwargs) -> dict: else: passphrase = ssh.passphrase + ssh_key = _return_correct_ssh_file(ssh) ssh_settings.setdefault('enabled', ssh.key_enabled) ssh_settings.setdefault('user', ssh.username) ssh_settings.setdefault('password', password) - ssh_key = f'{lib_path}/keys/{ssh.name}.pem' if ssh.key_enabled == 1 else '' ssh_settings.setdefault('key', ssh_key) ssh_settings.setdefault('passphrase', passphrase) @@ -66,10 +65,8 @@ def ssh_connect(server_ip): return ssh -def create_ssh_cred(name: str, password: str, group: int, username: str, enable: int, is_api: int) -> dict: - group_name = group_sql.get_group_name_by_id(group) +def create_ssh_cred(name: str, password: str, group: int, username: str, enable: int, is_api: int, shared: int) -> dict: lang = roxywi_common.get_user_lang_for_flask() - name = f'{name}_{group_name}' if password and password != "''": try: password = crypt_password(password) @@ -79,43 +76,20 @@ def create_ssh_cred(name: str, password: str, group: int, username: str, enable: password = '' try: - last_id = cred_sql.insert_new_ssh(name, enable, group, username, password) + last_id = cred_sql.insert_new_ssh(name, enable, group, username, password, shared) except Exception as e: return roxywi_common.handle_json_exceptions(e, 'Cannot create new SSH credentials') - roxywi_common.logging('RMON server', f'New SSH credentials {name} has been created', roxywi=1, login=1) + roxywi_common.logging('Roxy-WI server', f'New SSH credentials {name} has been created', roxywi=1, login=1) if is_api: return IdResponse(id=last_id).model_dump(mode='json') else: - data = render_template('ajax/new_ssh.html', groups=group_sql.select_groups(), sshs=cred_sql.select_ssh(name=name), lang=lang) + data = render_template('ajax/new_ssh.html', + groups=group_sql.select_groups(), sshs=cred_sql.select_ssh(name=name), lang=lang, adding=1 + ) return IdDataResponse(id=last_id, data=data).model_dump(mode='json') -def create_ssh_cred_api(name: str, enable: str, group: str, username: str, password: str) -> bool: - group_name = group_sql.get_group_name_by_id(group) - name = common.checkAjaxInput(name) - name = f'{name}_{group_name}' - enable = common.checkAjaxInput(enable) - username = common.checkAjaxInput(username) - password = common.checkAjaxInput(password) - - if password != '': - try: - password = crypt_password(password) - except Exception as e: - raise Exception(e) - - if username is None or name is None: - return False - else: - try: - cred_sql.insert_new_ssh(name, enable, group, username, password) - roxywi_common.logging('Roxy-WI server', f'New SSH credentials {name} has been created', roxywi=1) - return True - except Exception as e: - roxywi_common.handle_exceptions(e, 'Roxy-WI server', f'Cannot create SSH credentials {name}', roxywi=1) - - def upload_ssh_key(ssh_id: int, key: str, passphrase: str) -> None: key = key.replace("'", "") ssh = cred_sql.get_ssh(ssh_id) @@ -123,37 +97,23 @@ def upload_ssh_key(ssh_id: int, key: str, passphrase: str) -> None: lib_path = get_config.get_config_var('main', 'lib_path') full_dir = f'{lib_path}/keys/' name = ssh.name - ssh_keys = f'{name}.pem' + ssh_keys = f'{full_dir}{name}_{group_name}.pem' - try: - key = paramiko.pkey.load_private_key(key, password=passphrase) - except Exception as e: - raise Exception(e) + if key != '': + try: + key = paramiko.pkey.load_private_key(key, password=passphrase) + except Exception as e: + raise Exception(e) - try: - _ = name.split('_')[1] - split_name = True - except Exception: - split_name = False + try: + key.write_private_key_file(ssh_keys) + except Exception as e: + raise Exception(e) - if not os.path.isfile(ssh_keys) and not split_name: - name = f'{ssh.name}_{group_name}' - - if not os.path.exists(full_dir): - os.makedirs(full_dir) - - ssh_keys = f'{full_dir}{name}.pem' - - try: - key.write_private_key_file(ssh_keys) - except Exception as e: - raise Exception(e) - - try: - os.chmod(ssh_keys, 0o600) - except IOError as e: - roxywi_common.logging('RMON server', e.args[0], roxywi=1) - raise Exception(e) + try: + os.chmod(ssh_keys, 0o600) + except IOError as e: + raise Exception(e) if passphrase: try: @@ -168,54 +128,48 @@ def upload_ssh_key(ssh_id: int, key: str, passphrase: str) -> None: except Exception as e: raise Exception(e) - roxywi_common.logging("RMON server", f"A new SSH cert has been uploaded {ssh_keys}", roxywi=1, login=1) + roxywi_common.logging("Roxy-WI server", f"A new SSH cert has been uploaded {ssh_keys}", roxywi=1, login=1) -def update_ssh_key(ssh_id: int, name: str, password: str, enable: int, username: str, group: int) -> None: - lib_path = get_config.get_config_var('main', 'lib_path') +def update_ssh_key(body: CredRequest, group_id: int, ssh_id: int) -> None: ssh = cred_sql.get_ssh(ssh_id) - ssh_key_name = f'{lib_path}/keys/{ssh.name}.pem' - new_ssh_key_name = f'{lib_path}/keys/{name}.pem' + ssh_key_name = _return_correct_ssh_file(ssh) - if password != '': + if body.password != '' and body.password is not None: try: - password = crypt_password(password) + body.password = crypt_password(body.password) except Exception as e: raise Exception(e) - if ssh.key_enabled == 1 and os.path.isfile(ssh_key_name): + if os.path.isfile(ssh_key_name): + new_ssh_key_name = _return_correct_ssh_file(body) os.rename(ssh_key_name, new_ssh_key_name) os.chmod(new_ssh_key_name, 0o600) try: - cred_sql.update_ssh(ssh_id, name, enable, group, username, password) - roxywi_common.logging('RMON server', f'The SSH credentials {name} has been updated ', roxywi=1, login=1) + cred_sql.update_ssh(ssh_id, body.name, body.key_enabled, group_id, body.username, body.password, body.shared) + roxywi_common.logging('Roxy-WI server', f'The SSH credentials {body.name} has been updated ', roxywi=1, login=1) except Exception as e: raise Exception(e) -def delete_ssh_key(ssh_id) -> str: - lib_path = get_config.get_config_var('main', 'lib_path') +def delete_ssh_key(ssh_id) -> None: name = '' - ssh_enable = 0 - ssh_key_name = '' for sshs in cred_sql.select_ssh(id=ssh_id): - ssh_enable = sshs.key_enabled name = sshs.name - ssh_key_name = f'{lib_path}/keys/{sshs.name}.pem' - if ssh_enable == 1: - try: - os.remove(ssh_key_name) - except Exception: - pass + if sshs.key_enabled == 1: + ssh_key_name = _return_correct_ssh_file(sshs) + try: + os.remove(ssh_key_name) + except Exception: + pass try: cred_sql.delete_ssh(ssh_id) roxywi_common.logging('Roxy-WI server', f'The SSH credentials {name} has deleted', roxywi=1, login=1) - return 'ok' except Exception as e: - roxywi_common.handle_exceptions(e, 'Roxy-WI server', f'Cannot delete SSH credentials {name}', roxywi=1, login=1) + raise e def crypt_password(password: str) -> bytes: @@ -248,12 +202,11 @@ def decrypt_password(password: str) -> str: return decryp_pass -def get_creds(group_id: int = None, cred_id: int = None) -> list: +def get_creds(group_id: int = None, cred_id: int = None, not_shared: bool = False) -> list: json_data = [] - lib_path = get_config.get_config_var('main', 'lib_path') if group_id and cred_id: - creds = cred_sql.select_ssh(group=group_id, cred_id=cred_id) + creds = cred_sql.select_ssh(group=group_id, cred_id=cred_id, not_shared=not_shared) elif group_id: creds = cred_sql.select_ssh(group=group_id) else: @@ -262,15 +215,33 @@ def get_creds(group_id: int = None, cred_id: int = None) -> list: for cred in creds: cred_dict = model_to_dict(cred) cred_dict['name'] = cred_dict['name'].replace("'", "") - ssh_key_file = f'{lib_path}/keys/{cred_dict["name"]}.pem' - if os.path.isfile(ssh_key_file): - with open(ssh_key_file, 'rb') as key: - cred_dict['private_key'] = base64.b64encode(key.read()).decode('utf-8') + + if cred.key_enabled == 1: + ssh_key_file = _return_correct_ssh_file(cred) + if os.path.isfile(ssh_key_file): + with open(ssh_key_file, 'rb') as key: + cred_dict['private_key'] = base64.b64encode(key.read()).decode('utf-8') + else: + cred_dict['private_key'] = '' else: cred_dict['private_key'] = '' if cred_dict['password']: - cred_dict['password'] = decrypt_password(cred_dict['password']) + try: + cred_dict['password'] = decrypt_password(cred_dict['password']) + except Exception: + pass if cred_dict['passphrase']: cred_dict['passphrase'] = decrypt_password(cred_dict['passphrase']) json_data.append(cred_dict) return json_data + + +def _return_correct_ssh_file(cred: CredRequest) -> str: + lib_path = get_config.get_config_var('main', 'lib_path') + group_name = group_sql.get_group_name_by_id(cred.group_id) + # if not cred.key_enabled: + # return '' + if group_name not in cred.name: + return f'{lib_path}/keys/{cred.name}_{group_name}.pem' + else: + return f'{lib_path}/keys/{cred.name}.pem' diff --git a/app/static/js/admin/ssh.js b/app/static/js/admin/ssh.js index f16e6a82..e358eed1 100644 --- a/app/static/js/admin/ssh.js +++ b/app/static/js/admin/ssh.js @@ -61,6 +61,10 @@ function addCreds(dialog_id) { if ($('#new-ssh_enable').is(':checked')) { ssh_enable = '1'; } + let ssh_shared = 0; + if ($('#new-ssh_shared').is(':checked')) { + ssh_shared = '1'; + } let valid = true; let allFields = $([]).add(ssh_add_div).add($('#ssh_user')) allFields.removeClass("ui-state-error"); @@ -71,7 +75,8 @@ function addCreds(dialog_id) { "name": ssh_add_div.val(), "group_id": $('#new-sshgroup').val(), "username": $('#ssh_user').val(), - "pass": $('#ssh_pass').val(), + "password": $('#ssh_pass').val(), + "shared": ssh_shared, "key_enabled": ssh_enable, } $.ajax({ @@ -113,10 +118,14 @@ function sshKeyEnableShow(id) { function updateSSH(id) { toastr.clear(); let ssh_enable = 0; + let ssh_shared = 0; let ssh_name_val = $('#ssh_name-' + id).val(); if ($('#ssh_enable-' + id).is(':checked')) { ssh_enable = '1'; } + if ($('#ssh_shared-' + id).is(':checked')) { + ssh_shared = '1'; + } let group = $('#sshgroup-' + id).val(); if (group === undefined || group === null) { group = $('#new-sshgroup').val(); @@ -127,6 +136,7 @@ function updateSSH(id) { "key_enabled": ssh_enable, "username": $('#ssh_user-' + id).val(), "password": $('#ssh_pass-' + id).val(), + "shared": ssh_shared, } $.ajax({ url: "/server/cred/" + id, diff --git a/app/static/js/ha.js b/app/static/js/ha.js index 66192454..28bcd56f 100644 --- a/app/static/js/ha.js +++ b/app/static/js/ha.js @@ -221,7 +221,7 @@ function createHaClusterStep1(edited=false, cluster_id=0, clean=true) { toastr.error('error: Wrong VIP'); return false; } - jsonData = createJsonCluster('#enabled-check div div span'); + let jsonData = createJsonCluster('#enabled-check div div span'); if (!validateSlaves(jsonData)) { return false; } diff --git a/app/templates/ajax/new_ssh.html b/app/templates/ajax/new_ssh.html index b0b6c946..1e3f5ad1 100644 --- a/app/templates/ajax/new_ssh.html +++ b/app/templates/ajax/new_ssh.html @@ -1,4 +1,6 @@ +{% if adding %} {% import 'languages/'+lang|default('en')+'.html' as lang %} +{% endif %} {% for ssh in sshs %}
- {% set id = 'ssh_user-' + ssh.id|string() %} - {{ input(id, value=ssh.username, title='SSH user name') }} -
- {% if ssh.key_enabled == 1 %} - - {% else %} - - {% endif %} -