From 93857f90dbe4f6e5582aae8ceaa829e2427e30a4 Mon Sep 17 00:00:00 2001 From: luukas Date: Sat, 15 Jan 2022 02:23:50 +0200 Subject: [PATCH] JWT login and multi API keys! --- .../controllers/crafty_perms_controller.py | 5 + .../controllers/management_controller.py | 4 - app/classes/controllers/roles_controller.py | 4 +- .../controllers/server_perms_controller.py | 33 +- app/classes/controllers/servers_controller.py | 54 +- app/classes/controllers/users_controller.py | 71 +- app/classes/models/crafty_permissions.py | 17 +- app/classes/models/server_permissions.py | 75 +- app/classes/models/users.py | 99 ++- app/classes/shared/authentication.py | 76 ++ app/classes/shared/cmd.py | 27 +- app/classes/shared/helpers.py | 19 - app/classes/shared/main_controller.py | 33 +- app/classes/shared/main_models.py | 8 +- app/classes/shared/migration.py | 165 +---- app/classes/shared/permission_helper.py | 23 + app/classes/web/ajax_handler.py | 72 +- app/classes/web/api_handler.py | 23 +- app/classes/web/base_handler.py | 12 +- app/classes/web/panel_handler.py | 697 +++++++++--------- app/classes/web/public_handler.py | 25 +- app/classes/web/server_handler.py | 54 +- app/classes/web/upload_handler.py | 19 +- app/classes/web/websocket_handler.py | 34 +- app/config/config.json | 1 + app/frontend/templates/notify.html | 9 +- .../templates/panel/panel_config.html | 4 - .../templates/panel/panel_edit_role.html | 3 +- .../templates/panel/panel_edit_user.html | 14 +- .../panel/panel_edit_user_apikeys.html | 254 +++++++ app/migrations/20211120221511_api_keys.py | 12 + .../20211121233959_multi_api_keys.py | 23 + app/translations/en_EN.json | 5 + app/translations/fi_FI.json | 4 + app/translations/fr_FR.json | 4 + requirements.txt | 3 +- 36 files changed, 1254 insertions(+), 731 deletions(-) create mode 100644 app/classes/shared/authentication.py create mode 100644 app/classes/shared/permission_helper.py create mode 100644 app/frontend/templates/panel/panel_edit_user_apikeys.html create mode 100644 app/migrations/20211120221511_api_keys.py create mode 100644 app/migrations/20211121233959_multi_api_keys.py diff --git a/app/classes/controllers/crafty_perms_controller.py b/app/classes/controllers/crafty_perms_controller.py index f9f95f0d..b5f7f4e5 100644 --- a/app/classes/controllers/crafty_perms_controller.py +++ b/app/classes/controllers/crafty_perms_controller.py @@ -18,6 +18,7 @@ from app.classes.shared.server import Server from app.classes.minecraft.server_props import ServerProps from app.classes.minecraft.serverjars import server_jar_obj from app.classes.minecraft.stats import Stats +from app.classes.models.users import ApiKeys logger = logging.getLogger(__name__) @@ -70,3 +71,7 @@ class Crafty_Perms_Controller: @staticmethod def add_server_creation(user_id): return crafty_permissions.add_server_creation(user_id) + + @staticmethod + def get_api_key_permissions_list(key: ApiKeys): + return crafty_permissions.get_api_key_permissions_list(key) diff --git a/app/classes/controllers/management_controller.py b/app/classes/controllers/management_controller.py index c8900475..0ee72b64 100644 --- a/app/classes/controllers/management_controller.py +++ b/app/classes/controllers/management_controller.py @@ -31,10 +31,6 @@ class Management_Controller: def get_latest_hosts_stats(): return management_helper.get_latest_hosts_stats() - @staticmethod - def new_api_token(): - return management_helper.new_api_token() - #************************************************************************************************ # Commands Methods #************************************************************************************************ diff --git a/app/classes/controllers/roles_controller.py b/app/classes/controllers/roles_controller.py index f43de96a..aa1e936d 100644 --- a/app/classes/controllers/roles_controller.py +++ b/app/classes/controllers/roles_controller.py @@ -39,7 +39,9 @@ class Roles_Controller: @staticmethod - def update_role(role_id, role_data={}, permissions_mask="00000000"): + def update_role(role_id: str, role_data = None, permissions_mask: str = "00000000"): + if role_data is None: + role_data = {} base_data = Roles_Controller.get_role_with_servers(role_id) up_data = {} added_servers = set() diff --git a/app/classes/controllers/server_perms_controller.py b/app/classes/controllers/server_perms_controller.py index 314f9999..9f49f324 100644 --- a/app/classes/controllers/server_perms_controller.py +++ b/app/classes/controllers/server_perms_controller.py @@ -14,7 +14,7 @@ from app.classes.shared.console import console from app.classes.shared.main_models import db_helper from app.classes.models.server_permissions import server_permissions, Enum_Permissions_Server -from app.classes.models.users import users_helper +from app.classes.models.users import users_helper, ApiKeys from app.classes.models.roles import roles_helper from app.classes.models.servers import servers_helper @@ -42,11 +42,6 @@ class Server_Perms_Controller: permissions_list = server_permissions.get_role_permissions_list(role_id) return permissions_list - @staticmethod - def get_server_permissions_foruser(user_id, server_id): - permissions_list = server_permissions.get_user_permissions_list(user_id, server_id) - return permissions_list - @staticmethod def add_role_server(server_id, role_id, rs_permissions="00000000"): return server_permissions.add_role_server(server_id, role_id, rs_permissions) @@ -78,8 +73,30 @@ class Server_Perms_Controller: return server_permissions.get_role_permissions_list(role_id) @staticmethod - def get_user_permissions_list(user_id, server_id): - return server_permissions.get_user_permissions_list(user_id, server_id) + def get_user_id_permissions_list(user_id: str, server_id: str): + return server_permissions.get_user_id_permissions_list(user_id, server_id) + + @staticmethod + def get_api_key_id_permissions_list(key_id: str, server_id: str): + key = users_helper.get_user_api_key(key_id) + return server_permissions.get_api_key_permissions_list(key, server_id) + + @staticmethod + def get_api_key_permissions_list(key: ApiKeys, server_id: str): + return server_permissions.get_api_key_permissions_list(key, server_id) + + @staticmethod + def get_user_id_permissions_list(user_id: str, server_id: str): + return server_permissions.get_user_id_permissions_list(user_id, server_id) + + @staticmethod + def get_api_key_id_permissions_list(key_id: str, server_id: str): + key = users_helper.get_user_api_key(key_id) + return server_permissions.get_api_key_permissions_list(key, server_id) + + @staticmethod + def get_api_key_permissions_list(key: ApiKeys, server_id: str): + return server_permissions.get_api_key_permissions_list(key, server_id) @staticmethod def get_authorized_servers_stats_from_roles(user_id): diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index 2c7a56ee..fac54db0 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -17,7 +17,7 @@ from app.classes.shared.console import console from app.classes.shared.main_models import db_helper from app.classes.models.servers import servers_helper from app.classes.models.roles import roles_helper -from app.classes.models.users import users_helper +from app.classes.models.users import users_helper, ApiKeys from app.classes.models.server_permissions import server_permissions, Enum_Permissions_Server from app.classes.shared.server import Server @@ -82,18 +82,42 @@ class Servers_Controller: return servers_helper.get_all_servers_stats() @staticmethod - def get_authorized_servers_stats(user_id): + def get_authorized_servers_stats_api_key(api_key: ApiKeys): server_data = [] - authorized_servers = Servers_Controller.get_authorized_servers(user_id) + authorized_servers = Servers_Controller.get_authorized_servers(api_key.user.user_id) for s in authorized_servers: latest = servers_helper.get_latest_server_stats(s.get('server_id')) - user_permissions = server_permissions.get_user_permissions_list(user_id, s.get('server_id')) + key_permissions = server_permissions.get_api_key_permissions_list(api_key, s.get('server_id')) + if Enum_Permissions_Server.Commands in key_permissions: + user_command_permission = True + else: + user_command_permission = False + server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0], + "user_command_permission": user_command_permission}) + return server_data + + @staticmethod + def get_authorized_servers_stats(user_id): + server_data = [] + print('test 1') + authorized_servers = Servers_Controller.get_authorized_servers(user_id) + print('test 2') + + for s in authorized_servers: + latest = servers_helper.get_latest_server_stats(s.get('server_id')) + # TODO + user_permissions = server_permissions.get_user_id_permissions_list(user_id, s.get('server_id')) if Enum_Permissions_Server.Commands in user_permissions: user_command_permission = True else: user_command_permission = False - server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0], "user_command_permission":user_command_permission}) + server_data.append({ + 'server_data': s, + 'stats': db_helper.return_rows(latest)[0], + 'user_command_permission': user_command_permission + }) + return server_data @staticmethod @@ -112,17 +136,21 @@ class Servers_Controller: return servers_helper.server_id_exists(server_id) @staticmethod - def server_id_authorized(serverId, user_id): - authorized = 0 + def server_id_authorized(server_id_a, user_id): + print("Server id authorized: ") user_roles = users_helper.user_role_query(user_id) for role in user_roles: - authorized = server_permissions.get_role_servers_from_role_id(role.role_id) + for server_id_b in server_permissions.get_role_servers_from_role_id(role.role_id): + if server_id_a == server_id_b: + return True + return False - #authorized = db_helper.return_rows(authorized) - - if authorized.count() == 0: - return False - return True + @staticmethod + def server_id_authorized_api_key(server_id: str, api_key: ApiKeys) -> bool: + # TODO + return Servers_Controller.server_id_authorized(server_id, api_key.user.user_id) + # There is no view server permission + # permission_helper.both_have_perm(api_key) @staticmethod def set_update(server_id, value): diff --git a/app/classes/controllers/users_controller.py b/app/classes/controllers/users_controller.py index 71157e24..399da361 100644 --- a/app/classes/controllers/users_controller.py +++ b/app/classes/controllers/users_controller.py @@ -2,6 +2,8 @@ import os import time import logging import sys +from typing import Optional + import yaml import asyncio import shutil @@ -13,6 +15,7 @@ from app.classes.shared.helpers import helper from app.classes.shared.console import console from app.classes.models.users import Users, users_helper +from app.classes.shared.authentication import authentication from app.classes.models.crafty_permissions import crafty_permissions, Enum_Permissions_Crafty from app.classes.models.management import management_helper @@ -31,10 +34,6 @@ class Users_Controller: def get_id_by_name(username): return users_helper.get_user_id_by_name(username) - @staticmethod - def get_user_by_api_token(token: str): - return users_helper.get_user_by_api_token(token) - @staticmethod def get_user_lang_by_id(user_id): return users_helper.get_user_lang_by_id(user_id) @@ -52,7 +51,11 @@ class Users_Controller: users_helper.set_support_path(user_id, support_path) @staticmethod - def update_user(user_id, user_data={}, user_crafty_data={}): + def update_user(user_id: str, user_data=None, user_crafty_data=None): + if user_crafty_data is None: + user_crafty_data = {} + if user_data is None: + user_data = {} base_data = users_helper.get_user(user_id) up_data = {} added_roles = set() @@ -64,9 +67,6 @@ class Users_Controller: elif key == "roles": added_roles = user_data['roles'].difference(base_data['roles']) removed_roles = base_data['roles'].difference(user_data['roles']) - elif key == "regen_api": - if user_data['regen_api']: - up_data['api_token'] = management_helper.new_api_token() elif key == "password": if user_data['password'] is not None and user_data['password'] != "": up_data['password'] = helper.encode_pass(user_data['password']) @@ -77,13 +77,12 @@ class Users_Controller: logger.debug("user: {} +role:{} -role:{}".format(user_data, added_roles, removed_roles)) for role in added_roles: users_helper.get_or_create(user_id=user_id, role_id=role) - # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point + permissions_mask = user_crafty_data.get('permissions_mask', '000') + + if 'server_quantity' in user_crafty_data: + limit_server_creation = user_crafty_data['server_quantity'][ + Enum_Permissions_Crafty.Server_Creation.name] - for key in user_crafty_data: - if key == "permissions_mask": - permissions_mask = user_crafty_data['permissions_mask'] - if key == "server_quantity": - limit_server_creation = user_crafty_data['server_quantity'][Enum_Permissions_Crafty.Server_Creation.name] limit_user_creation = user_crafty_data['server_quantity'][Enum_Permissions_Crafty.User_Config.name] limit_role_creation = user_crafty_data['server_quantity'][Enum_Permissions_Crafty.Roles_Config.name] else: @@ -98,8 +97,8 @@ class Users_Controller: users_helper.update_user(user_id, up_data) @staticmethod - def add_user(username, password=None, email="default@example.com", api_token=None, enabled=True, superuser=False): - return users_helper.add_user(username, password=password, email=email, api_token=api_token, enabled=enabled, superuser=superuser) + def add_user(username, password=None, email="default@example.com", enabled: bool = True, superuser: bool = False): + return users_helper.add_user(username, password=password, email=email, enabled=enabled, superuser=superuser) @staticmethod def remove_user(user_id): @@ -109,9 +108,19 @@ class Users_Controller: def user_id_exists(user_id): return users_helper.user_id_exists(user_id) - #************************************************************************************************ + @staticmethod + def get_user_id_by_api_token(token: str) -> str: + token_data = authentication.check_no_iat(token) + return token_data['user_id'] + + @staticmethod + def get_user_by_api_token(token: str): + _, user = authentication.check(token) + return user + + # ************************************************************************************************ # User Roles Methods - #************************************************************************************************ + # ************************************************************************************************ @staticmethod def get_user_roles_id(user_id): @@ -132,3 +141,29 @@ class Users_Controller: @staticmethod def user_role_query(user_id): return users_helper.user_role_query(user_id) + + # ************************************************************************************************ + # Api Keys Methods + # ************************************************************************************************ + + @staticmethod + def get_user_api_keys(user_id: str): + return users_helper.get_user_api_keys(user_id) + + @staticmethod + def get_user_api_key(key_id: str): + return users_helper.get_user_api_key(key_id) + + @staticmethod + def add_user_api_key(name: str, user_id: str, superuser: bool = False, + server_permissions_mask: Optional[str] = None, + crafty_permissions_mask: Optional[str] = None): + return users_helper.add_user_api_key(name, user_id, superuser, server_permissions_mask, crafty_permissions_mask) + + @staticmethod + def delete_user_api_keys(user_id: str): + return users_helper.delete_user_api_keys(user_id) + + @staticmethod + def delete_user_api_key(key_id: str): + return users_helper.delete_user_api_key(key_id) diff --git a/app/classes/models/crafty_permissions.py b/app/classes/models/crafty_permissions.py index dbd51232..c0bc9c6b 100644 --- a/app/classes/models/crafty_permissions.py +++ b/app/classes/models/crafty_permissions.py @@ -5,7 +5,8 @@ import datetime from app.classes.shared.helpers import helper from app.classes.shared.console import console -from app.classes.models.users import Users +from app.classes.models.users import Users, ApiKeys +from app.classes.shared.permission_helper import permission_helper logger = logging.getLogger(__name__) peewee_logger = logging.getLogger('peewee') @@ -191,4 +192,18 @@ class Permissions_Crafty: User_Crafty.save(user_crafty) return user_crafty.created_server + @staticmethod + def get_api_key_permissions_list(key: ApiKeys): + user = key.user + if user.superuser and key.superuser: + return crafty_permissions.get_permissions_list() + else: + user_permissions_mask = crafty_permissions.get_crafty_permissions_mask(user.user_id) + key_permissions_mask: str = key.crafty_permissions + permissions_mask = permission_helper.combine_masks(user_permissions_mask, key_permissions_mask) + permissions_list = crafty_permissions.get_permissions(permissions_mask) + return permissions_list + + + crafty_permissions = Permissions_Crafty() \ No newline at end of file diff --git a/app/classes/models/server_permissions.py b/app/classes/models/server_permissions.py index f6eaf4c4..165c2b5b 100644 --- a/app/classes/models/server_permissions.py +++ b/app/classes/models/server_permissions.py @@ -7,7 +7,8 @@ from app.classes.shared.helpers import helper from app.classes.shared.console import console from app.classes.models.servers import Servers from app.classes.models.roles import Roles -from app.classes.models.users import users_helper +from app.classes.models.users import users_helper, ApiKeys, Users +from app.classes.shared.permission_helper import permission_helper logger = logging.getLogger(__name__) peewee_logger = logging.getLogger('peewee') @@ -78,10 +79,7 @@ class Permissions_Servers: @staticmethod def has_permission(permission_mask, permission_tested: Enum_Permissions_Server): - result = False - if permission_mask[permission_tested.value] == '1': - result = True - return result + return permission_mask[permission_tested.value] == '1' @staticmethod def set_permission(permission_mask, permission_tested: Enum_Permissions_Server, value): @@ -94,6 +92,14 @@ class Permissions_Servers: def get_permission(permission_mask, permission_tested: Enum_Permissions_Server): return permission_mask[permission_tested.value] + @staticmethod + def get_token_permissions(permissions_mask, api_permissions_mask): + permissions_list = [] + for member in Enum_Permissions_Server.__members__.items(): + if permission_helper.both_have_perm(permissions_mask, api_permissions_mask, member[1]): + permissions_list.append(member[1]) + return permissions_list + #************************************************************************************************ # Role_Servers Methods @@ -146,7 +152,9 @@ class Permissions_Servers: Role_Servers.save(role_server) @staticmethod - def delete_roles_permissions(role_id, removed_servers={}): + def delete_roles_permissions(role_id, removed_servers=None): + if removed_servers is None: + removed_servers = {} return Role_Servers.delete().where(Role_Servers.role_id == role_id).where(Role_Servers.server_id.in_(removed_servers)).execute() @staticmethod @@ -155,21 +163,52 @@ class Permissions_Servers: return Role_Servers.delete().where(Role_Servers.server_id == server_id).execute() @staticmethod - def get_user_permissions_list(user_id, server_id): - permissions_mask = '' - permissions_list = [] + def get_user_id_permissions_mask(user_id, server_id: str): + user = users_helper.get_user_model(user_id) + return server_permissions.get_user_permissions_mask(user, server_id) - user = users_helper.get_user(user_id) - if user['superuser'] == True: + @staticmethod + def get_user_permissions_mask(user: Users, server_id: str): + if user.superuser: + permissions_mask = '1' * len(server_permissions.get_permissions_list()) + else: + roles_list = users_helper.get_user_roles_id(user['user_id']) + role_server = Role_Servers.select().where(Role_Servers.role_id.in_(roles_list)).where(Role_Servers.server_id == server_id).execute() + permissions_mask = role_server[0].permissions + return permissions_mask + + @staticmethod + def get_user_id_permissions_list(user_id, server_id: str): + user = users_helper.get_user_model(user_id) + return server_permissions.get_user_permissions_list(user, server_id) + + @staticmethod + def get_user_permissions_list(user: Users, server_id: str): + if user.superuser: permissions_list = server_permissions.get_permissions_list() else: - roles_list = users_helper.get_user_roles_id(user_id) - role_server = Role_Servers.select().where(Role_Servers.role_id.in_(roles_list)).where(Role_Servers.server_id == int(server_id)).execute() - if len(role_server) > 0: - permissions_mask = role_server[0].permissions - else: - permissions_mask = '00000000' + permissions_mask = server_permissions.get_user_permissions_mask(user, server_id) permissions_list = server_permissions.get_permissions(permissions_mask) return permissions_list -server_permissions = Permissions_Servers() \ No newline at end of file + @staticmethod + def get_api_key_id_permissions_list(key_id, server_id: str): + key = ApiKeys.get(ApiKeys.token_id == key_id) + return server_permissions.get_api_key_permissions_list(key, server_id) + + @staticmethod + def get_api_key_permissions_list(key: ApiKeys, server_id: str): + user = key.user + if user.superuser and key.superuser: + return server_permissions.get_permissions_list() + else: + roles_list = users_helper.get_user_roles_id(user['user_id']) + role_server = Role_Servers.select().where(Role_Servers.role_id.in_(roles_list)).where(Role_Servers.server_id == server_id).execute() + user_permissions_mask = role_server[0].permissions + key_permissions_mask = key.server_permissions + permissions_mask = permission_helper.combine_masks(user_permissions_mask, key_permissions_mask) + permissions_list = server_permissions.get_permissions(permissions_mask) + return permissions_list + + +server_permissions = Permissions_Servers() diff --git a/app/classes/models/users.py b/app/classes/models/users.py index 3cf91804..4b6c10cc 100644 --- a/app/classes/models/users.py +++ b/app/classes/models/users.py @@ -2,6 +2,7 @@ import os import sys import logging import datetime +from typing import Optional, List, Union from app.classes.shared.helpers import helper from app.classes.shared.console import console @@ -41,14 +42,32 @@ class Users(Model): email = CharField(default="default@example.com") enabled = BooleanField(default=True) superuser = BooleanField(default=False) - api_token = CharField(default="", unique=True, index=True) # we may need to revisit this lang = CharField(default="en_EN") support_logs = CharField(default = '') + valid_tokens_from = DateTimeField(default=datetime.datetime.now) class Meta: table_name = "users" database = database + +# ************************************************************************************************ +# API Keys Class +# ************************************************************************************************ +class ApiKeys(Model): + token_id = AutoField() + name = CharField(default='', unique=True, index=True) + created = DateTimeField(default=datetime.datetime.now) + user = ForeignKeyField(Users, backref='api_token', index=True) + server_permissions = CharField(default='00000000') + crafty_permissions = CharField(default='000') + superuser = BooleanField(default=False) + + class Meta: + table_name = 'api_keys' + database = database + + #************************************************************************************************ # User Roles Class #************************************************************************************************ @@ -86,18 +105,6 @@ class helper_users: except DoesNotExist: return None - @staticmethod - def get_user_by_api_token(token: str): - query = Users.select().where(Users.api_token == token) - - if query.exists(): - user = model_to_dict(Users.get(Users.api_token == token)) - # I know it should apply it without setting it but I'm just making sure - user = users_helper.add_user_roles(user) - return user - else: - return {} - @staticmethod def user_query(user_id): user_query = Users.select().where(Users.user_id == user_id) @@ -117,7 +124,6 @@ class helper_users: 'email': "default@example.com", 'enabled': True, 'superuser': True, - 'api_token': None, 'roles': [], 'servers': [], 'support_logs': '', @@ -140,21 +146,21 @@ class helper_users: return False @staticmethod - def add_user(username, password=None, email=None, api_token=None, enabled=True, superuser=False): + def get_user_model(user_id: str) -> Users: + user = Users.get(Users.user_id == user_id) + user = users_helper.add_user_roles(user) + return user + + @staticmethod + def add_user(username: str, password: Optional[str] = None, email: Optional[str] = None, enabled: bool = True, superuser: bool = False) -> str: if password is not None: pw_enc = helper.encode_pass(password) else: pw_enc = None - if api_token is None: - api_token = users_helper.new_api_token() - else: - if type(api_token) is not str and len(api_token) != 32: - raise ValueError("API token must be a 32 character string") user_id = Users.insert({ Users.username: username.lower(), Users.password: pw_enc, Users.email: email, - Users.api_token: api_token, Users.enabled: enabled, Users.superuser: superuser, Users.created: helper.get_time_as_string() @@ -162,7 +168,9 @@ class helper_users: return user_id @staticmethod - def update_user(user_id, up_data={}): + def update_user(user_id, up_data=None): + if up_data is None: + up_data = {} if up_data: Users.update(up_data).where(Users.user_id == user_id).execute() @@ -183,14 +191,6 @@ class helper_users: return False return True - @staticmethod - def new_api_token(): - while True: - token = helper.random_string_generator(32) - test = list(Users.select(Users.user_id).where(Users.api_token == token)) - if len(test) == 0: - return token - #************************************************************************************************ # User_Roles Methods #************************************************************************************************ @@ -223,7 +223,7 @@ class helper_users: }).execute() @staticmethod - def add_user_roles(user): + def add_user_roles(user: Union[dict, Users]): if type(user) == dict: user_id = user['user_id'] else: @@ -237,7 +237,11 @@ class helper_users: for r in roles_query: roles.add(r.role_id.role_id) - user['roles'] = roles + if type(user) == dict: + user['roles'] = roles + else: + user.roles = roles + #logger.debug("user: ({}) {}".format(user_id, user)) return user @@ -257,5 +261,36 @@ class helper_users: def remove_roles_from_role_id(role_id): User_Roles.delete().where(User_Roles.role_id == role_id).execute() +# ************************************************************************************************ +# ApiKeys Methods +# ************************************************************************************************ + + @staticmethod + def get_user_api_keys(user_id: str): + return ApiKeys.select().where(ApiKeys.user_id == user_id).execute() + + @staticmethod + def get_user_api_key(key_id: str) -> ApiKeys: + return ApiKeys.get(ApiKeys.token_id == key_id) + + @staticmethod + def add_user_api_key(name: str, user_id: str, superuser: bool = False, server_permissions_mask: Optional[str] = None, crafty_permissions_mask: Optional[str] = None): + return ApiKeys.insert({ + ApiKeys.name: name, + ApiKeys.user_id: user_id, + **({ApiKeys.server_permissions: server_permissions_mask} if server_permissions_mask is not None else {}), + **({ApiKeys.crafty_permissions: crafty_permissions_mask} if crafty_permissions_mask is not None else {}), + ApiKeys.superuser: superuser + }).execute() + + @staticmethod + def delete_user_api_keys(user_id: str): + ApiKeys.delete().where(ApiKeys.user_id == user_id).execute() + + @staticmethod + def delete_user_api_key(key_id: str): + ApiKeys.delete().where(ApiKeys.token_id == key_id).execute() + + users_helper = helper_users() \ No newline at end of file diff --git a/app/classes/shared/authentication.py b/app/classes/shared/authentication.py new file mode 100644 index 00000000..c0c6a693 --- /dev/null +++ b/app/classes/shared/authentication.py @@ -0,0 +1,76 @@ +import logging +import time +from typing import Optional, Dict, Any, Tuple + +import jwt +from jwt import PyJWTError + +from app.classes.models.users import users_helper, ApiKeys +from app.classes.shared.helpers import helper + +logger = logging.getLogger(__name__) + + +class Authentication: + def __init__(self): + self.secret = "my secret" + self.secret = helper.get_setting('apikey_secret', None) + + if self.secret is None or self.secret == 'random': + self.secret = helper.random_string_generator(64) + + @staticmethod + def generate(user_id, extra=None): + if extra is None: + extra = {} + return jwt.encode( + { + 'user_id': user_id, + 'iat': int(time.time()), + **extra + }, + authentication.secret, + algorithm="HS256" + ) + + @staticmethod + def read(token): + return jwt.decode(token, authentication.secret, algorithms=["HS256"]) + + @staticmethod + def check_no_iat(token) -> Optional[Dict[str, Any]]: + try: + return jwt.decode(token, authentication.secret, algorithms=["HS256"]) + except PyJWTError as error: + logger.debug("Error while checking JWT token: ", exc_info=error) + return None + + @staticmethod + def check(token) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]: + try: + data = jwt.decode(token, authentication.secret, algorithms=["HS256"]) + except PyJWTError as error: + logger.debug("Error while checking JWT token: ", exc_info=error) + return None + iat: int = data['iat'] + key: Optional[ApiKeys] = None + if 'token_id' in data: + key_id = data['token_id'] + key = users_helper.get_user_api_key(key_id) + if key is None: + return None + user_id: str = data['user_id'] + user = users_helper.get_user(user_id) + # TODO: Have a cache or something so we don't constantly have to query the database + if int(user.get('valid_tokens_from').timestamp()) < iat: + # Success! + return key, data, user + else: + return None + + @staticmethod + def check_bool(token) -> bool: + return authentication.check(token) is not None + + +authentication = Authentication() diff --git a/app/classes/shared/cmd.py b/app/classes/shared/cmd.py index bed2e798..d04eda30 100644 --- a/app/classes/shared/cmd.py +++ b/app/classes/shared/cmd.py @@ -41,29 +41,33 @@ class MainPrompt(cmd.Cmd, object): self.universal_exit() def do_migrations(self, line): - if (line == 'up'): + if line == 'up': self.migration_manager.up() - elif (line == 'down'): + elif line == 'down': self.migration_manager.down() - elif (line == 'done'): + elif line == 'done': console.info(self.migration_manager.done) - elif (line == 'todo'): + elif line == 'todo': console.info(self.migration_manager.todo) - elif (line == 'diff'): + elif line == 'diff': console.info(self.migration_manager.diff) - elif (line == 'info'): + elif line == 'info': console.info('Done: {}'.format(self.migration_manager.done)) console.info('FS: {}'.format(self.migration_manager.todo)) console.info('Todo: {}'.format(self.migration_manager.diff)) - elif (line.startswith('add ')): + elif line.startswith('add '): migration_name = line[len('add '):] self.migration_manager.create(migration_name, False) else: console.info('Unknown migration command') - - def do_threads(self, line): + + @staticmethod + def do_threads(_line): for thread in threading.enumerate(): - print(f'Name: {thread.name} IDENT: {thread.ident}') + if sys.version_info >= (3, 8): + print(f'Name: {thread.name} Identifier: {thread.ident} TID/PID: {thread.native_id}') + else: + print(f'Name: {thread.name} Identifier: {thread.ident}') def universal_exit(self): logger.info("Stopping all server daemons / threads") @@ -75,11 +79,10 @@ class MainPrompt(cmd.Cmd, object): sys.exit(0) time.sleep(1) - @staticmethod def help_exit(): console.help("Stops the server if running, Exits the program") - + @staticmethod def help_migrations(): console.help("Only for advanced users. Use with caution") diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 3b788056..42e91854 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -164,25 +164,6 @@ class Helpers: cmd_out[ci] += c return cmd_out - def check_for_old_logs(self, db_helper): - servers = db_helper.get_all_defined_servers() - for server in servers: - logs_path = os.path.split(server['log_path'])[0] - latest_log_file = os.path.split(server['log_path'])[1] - logs_delete_after = int(server['logs_delete_after']) - if logs_delete_after == 0: - continue - - log_files = list(filter( - lambda val: val != latest_log_file, - os.listdir(logs_path) - )) - for log_file in log_files: - log_file_path = os.path.join(logs_path, log_file) - if self.check_file_exists(log_file_path) and \ - self.is_file_older_than_x_days(log_file_path, logs_delete_after): - os.remove(log_file_path) - def get_setting(self, key, default_return=False): try: diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index 8e1615aa..8bf438a9 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -3,6 +3,8 @@ import pathlib import time import logging import sys +from typing import Union + from app.classes.models.server_permissions import Enum_Permissions_Server from app.classes.models.users import helper_users from peewee import DoesNotExist @@ -18,10 +20,16 @@ from app.classes.web.websocket_helper import websocket_helper from app.classes.shared.helpers import helper from app.classes.shared.console import console -#Importing Models -from app.classes.models.crafty_permissions import crafty_permissions, Enum_Permissions_Crafty +# Importing Models from app.classes.models.servers import servers_helper -#Importing Controllers +from app.classes.shared.console import console +from app.classes.shared.helpers import helper +from app.classes.shared.server import Server +from app.classes.minecraft.server_props import ServerProps +from app.classes.minecraft.serverjars import server_jar_obj +from app.classes.minecraft.stats import Stats + +# Importing Controllers from app.classes.controllers.crafty_perms_controller import Crafty_Perms_Controller from app.classes.controllers.management_controller import Management_Controller from app.classes.controllers.users_controller import Users_Controller @@ -29,11 +37,6 @@ from app.classes.controllers.roles_controller import Roles_Controller from app.classes.controllers.server_perms_controller import Server_Perms_Controller from app.classes.controllers.servers_controller import Servers_Controller -from app.classes.shared.server import Server -from app.classes.minecraft.server_props import ServerProps -from app.classes.minecraft.serverjars import server_jar_obj -from app.classes.minecraft.stats import Stats - logger = logging.getLogger(__name__) class Controller: @@ -173,7 +176,7 @@ class Controller: @staticmethod def add_system_user(): - helper_users.add_user("system", helper.random_string_generator(64), "default@example.com", helper_users.new_api_token(), False, False) + helper_users.add_user("system", helper.random_string_generator(64), "default@example.com", False, False) def get_server_settings(self, server_id): for s in self.servers_list: @@ -183,17 +186,17 @@ class Controller: logger.warning("Unable to find server object for server id {}".format(server_id)) return False - def get_server_obj(self, server_id): + def get_server_obj(self, server_id: Union[str, int]) -> Union[bool, Server]: for s in self.servers_list: - if int(s['server_id']) == int(server_id): + if str(s['server_id']) == str(server_id): return s['server_obj'] logger.warning("Unable to find server object for server id {}".format(server_id)) - return False + return False # TODO: Change to None - def get_server_data(self, server_id): + def get_server_data(self, server_id: str): for s in self.servers_list: - if int(s['server_id']) == int(server_id): + if s['server_id'] == server_id: return s['server_data_obj'] logger.warning("Unable to find server object for server id {}".format(server_id)) @@ -406,7 +409,7 @@ class Controller: for s in self.servers_list: # if this is the droid... im mean server we are looking for... - if int(s['server_id']) == int(server_id): + if s['server_id'] == server_id: server_data = self.get_server_data(server_id) server_name = server_data['server_name'] backup_dir = self.servers.get_server_data_by_id(server_id)['backup_path'] diff --git a/app/classes/shared/main_models.py b/app/classes/shared/main_models.py index 01d12951..d2137184 100644 --- a/app/classes/shared/main_models.py +++ b/app/classes/shared/main_models.py @@ -10,6 +10,10 @@ from app.classes.minecraft.server_props import ServerProps from app.classes.web.websocket_helper import websocket_helper +# To disable warning about unused import ; Users is imported from here in other places +Users = Users + + logger = logging.getLogger(__name__) peewee_logger = logging.getLogger('peewee') peewee_logger.setLevel(logging.INFO) @@ -39,20 +43,16 @@ class db_builder: username = default_data.get("username", 'admin') password = default_data.get("password", 'crafty') - #api_token = helper.random_string_generator(32) # #Users.insert({ # Users.username: username.lower(), # Users.password: helper.encode_pass(password), - # Users.api_token: api_token, # Users.enabled: True, # Users.superuser: True #}).execute() user_id = users_helper.add_user(username=username, password=password, email="default@example.com", superuser=True) #users_helper.update_user(user_id, user_crafty_data={"permissions_mask":"111", "server_quantity":[-1,-1,-1]} ) - #console.info("API token is {}".format(api_token)) - @staticmethod def is_fresh_install(): try: diff --git a/app/classes/shared/migration.py b/app/classes/shared/migration.py index 8baa4673..3538eeeb 100644 --- a/app/classes/shared/migration.py +++ b/app/classes/shared/migration.py @@ -4,13 +4,9 @@ import typing as t import sys import os import re -from importlib import import_module from functools import wraps -try: - from functools import cached_property -except ImportError: - from cached_property import cached_property +from functools import cached_property from app.classes.shared.helpers import helper from app.classes.shared.console import console @@ -21,7 +17,7 @@ try: import peewee from playhouse.migrate import ( SchemaMigrator as ScM, - SqliteMigrator as SqM, + SqliteMigrator, Operation, SQL, operation, SqliteDatabase, make_index_name, Context ) @@ -32,6 +28,22 @@ except ModuleNotFoundError as e: console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) +MIGRATE_TABLE = 'migratehistory' +MIGRATE_TEMPLATE = '''# Generated by database migrator +import peewee + +def migrate(migrator, db): + """ + Write your migrations here. + """ +{migrate} + +def rollback(migrator, db): + """ + Write your rollback migrations here. + """ +{rollback}''' + class MigrateHistory(peewee.Model): """ @@ -41,30 +53,15 @@ class MigrateHistory(peewee.Model): name = peewee.CharField(unique=True) migrated_at = peewee.DateTimeField(default=datetime.utcnow) + # noinspection PyTypeChecker def __unicode__(self) -> str: """ String representation of this migration """ return self.name - -MIGRATE_TABLE = 'migratehistory' -MIGRATE_TEMPLATE = '''# Generated by database migrator - - -def migrate(migrator, database, **kwargs): - """ - Write your migrations here. - """ -{migrate} - - -def rollback(migrator, database, **kwargs): - """ - Write your rollback migrations here. - """ -{rollback}''' -VOID: t.Callable = lambda m, d: None + class Meta: + table_name = MIGRATE_TABLE def get_model(method): @@ -75,11 +72,12 @@ def get_model(method): @wraps(method) def wrapper(migrator, model, *args, **kwargs): if isinstance(model, str): - return method(migrator, migrator.orm[model], *args, **kwargs) + return method(migrator, migrator.table_dict[model], *args, **kwargs) return method(migrator, model, *args, **kwargs) return wrapper +# noinspection PyProtectedMember class Migrator(object): def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]): """ @@ -88,8 +86,8 @@ class Migrator(object): if isinstance(database, peewee.Proxy): database = database.obj self.database: SqliteDatabase = database - self.orm: t.Dict[str, peewee.Model] = {} - self.operations: t.List[Operation] = [] + self.table_dict: t.Dict[str, peewee.Model] = {} + self.operations: t.List[t.Union[Operation, callable]] = [] self.migrator = SqliteMigrator(database) def run(self): @@ -113,13 +111,13 @@ class Migrator(object): """ Executes raw SQL. """ - self.operations.append(self.migrator.sql(sql, *params)) + self.operations.append(SQL(sql, *params)) def create_table(self, model: peewee.Model) -> peewee.Model: """ Creates model and table in database. """ - self.orm[model._meta.table_name] = model + self.table_dict[model._meta.table_name] = model model._meta.database = self.database self.operations.append(model.create_table) return model @@ -129,8 +127,8 @@ class Migrator(object): """ Drops model and table from database. """ - del self.orm[model._meta.table_name] - self.operations.append(self.migrator.drop_table(model)) + del self.table_dict[model._meta.table_name] + self.operations.append(lambda: model.drop_table(cascade=False)) @get_model def add_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model: @@ -147,64 +145,16 @@ class Migrator(object): return model @get_model - def change_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model: - """ - Changes fields. - """ - for name, field in fields.items(): - old_field = model._meta.fields.get(name, field) - old_column_name = old_field and old_field.column_name - - model._meta.add_field(name, field) - - if isinstance(old_field, peewee.ForeignKeyField): - self.operations.append(self.migrator.drop_foreign_key_constraint( - model._meta.table_name, old_column_name)) - - if old_column_name != field.column_name: - self.operations.append( - self.migrator.rename_column( - model._meta.table_name, old_column_name, field.column_name)) - - if isinstance(field, peewee.ForeignKeyField): - on_delete = field.on_delete if field.on_delete else 'RESTRICT' - on_update = field.on_update if field.on_update else 'RESTRICT' - self.operations.append(self.migrator.add_foreign_key_constraint( - model._meta.table_name, field.column_name, - field.rel_model._meta.table_name, field.rel_field.name, - on_delete, on_update)) - continue - - self.operations.append(self.migrator.change_column( - model._meta.table_name, field.column_name, field)) - - if field.unique == old_field.unique: - continue - - if field.unique: - index = (field.column_name,), field.unique - self.operations.append(self.migrator.add_index( - model._meta.table_name, *index)) - model._meta.indexes.append(index) - else: - index = (field.column_name,), old_field.unique - self.operations.append(self.migrator.drop_index( - model._meta.table_name, *index)) - model._meta.indexes.remove(index) - - return model - - @get_model - def drop_columns(self, model: peewee.Model, names: str, **kwargs) -> peewee.Model: + def drop_columns(self, model: peewee.Model, names: str) -> peewee.Model: """ Removes fields from model. """ fields = [field for field in model._meta.fields.values() if field.name in names] - cascade = kwargs.pop('cascade', True) for field in fields: self.__del_field__(model, field) if field.unique: + # Drop unique index index_name = make_index_name( model._meta.table_name, [field.column_name]) self.operations.append(self.migrator.drop_index( @@ -250,16 +200,15 @@ class Migrator(object): Renames table in database. """ old_name = model._meta.table_name - del self.orm[model._meta.table_name] + del self.table_dict[model._meta.table_name] model._meta.table_name = new_name - self.orm[model._meta.table_name] = model + self.table_dict[model._meta.table_name] = model self.operations.append(self.migrator.rename_table(old_name, new_name)) return model @get_model - def add_index(self, model: peewee.Model, *columns: str, **kwargs) -> peewee.Model: + def add_index(self, model: peewee.Model, *columns: str, unique=False) -> peewee.Model: """Create indexes.""" - unique = kwargs.pop('unique', False) model._meta.indexes.append((columns, unique)) columns_ = [] for col in columns: @@ -329,42 +278,8 @@ class Migrator(object): return model -class SqliteMigrator(SqM): - def drop_table(self, model): - return lambda: model.drop_table(cascade=False) - - @operation - def change_column(self, table: str, column_name: str, field: peewee.Field): - operations = [self.alter_change_column(table, column_name, field)] - if not field.null: - operations.extend([self.add_not_null(table, column_name)]) - return operations - - def alter_change_column(self, table: str, column_name: str, field: peewee.Field) -> Operation: - return self._update_column(table, column_name, lambda x, y: y) - - @operation - def sql(self, sql: str, *params) -> SQL: - """ - Executes raw SQL. - """ - return SQL(sql, *params) - - def alter_add_column( - self, table: str, column_name: str, field: peewee.Field, **kwargs) -> Operation: - """ - Fixes field name for ForeignKeys. - """ - name = field.name - op = super().alter_add_column( - table, column_name, field, **kwargs) - if isinstance(field, peewee.ForeignKeyField): - field.name = name - return op - - +# noinspection PyProtectedMember class MigrationManager(object): - filemask = re.compile(r"[\d]+_[^\.]+\.py$") def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]): @@ -376,7 +291,7 @@ class MigrationManager(object): self.database = database @cached_property - def model(self) -> peewee.Model: + def model(self) -> t.Type[MigrateHistory]: """ Initialize and cache the MigrationHistory model. """ @@ -487,7 +402,7 @@ class MigrationManager(object): scope = {} code = compile(code, '', 'exec', dont_inherit=True) exec(code, scope, None) - return scope.get('migrate', VOID), scope.get('rollback', VOID) + return scope.get('migrate', lambda m, d: None), scope.get('rollback', lambda m, d: None) def up_one(self, name: str, migrator: Migrator, fake: bool = False, rollback: bool = False) -> str: @@ -518,11 +433,11 @@ class MigrationManager(object): except Exception: self.database.rollback() - operation = 'Rollback' if rollback else 'Migration' - logger.exception('{} failed: {}'.format(operation, name)) + operation_name = 'Rollback' if rollback else 'Migration' + logger.exception('{} failed: {}'.format(operation_name, name)) raise - def down(self, name: t.Optional[str] = None): + def down(self): """ Rolls back migrations. """ diff --git a/app/classes/shared/permission_helper.py b/app/classes/shared/permission_helper.py new file mode 100644 index 00000000..9b14ac0f --- /dev/null +++ b/app/classes/shared/permission_helper.py @@ -0,0 +1,23 @@ +from enum import Enum + + +class PermissionHelper: + @staticmethod + def both_have_perm(a: str, b: str, permission_tested: Enum): + return permission_helper.combine_perm_bool(a[permission_tested.value], b[permission_tested.value]) + + @staticmethod + def combine_perm(a: str, b: str) -> str: + return '1' if (a == '1' and b == '1') else '0' + + @staticmethod + def combine_perm_bool(a: str, b: str) -> bool: + return a == '1' and b == '1' + + @staticmethod + def combine_masks(permission_mask_a: str, permission_mask_b: str) -> str: + both_masks = zip(list(permission_mask_a), list(permission_mask_b)) + return ''.join(map(lambda x: permission_helper.combine_perm(x[0], x[1]), both_masks)) + + +permission_helper = PermissionHelper() diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index f05bba3b..df2da540 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -35,13 +35,13 @@ class AjaxHandler(BaseHandler): @tornado.web.authenticated def get(self, page): - user_data = json.loads(self.get_secure_cookie("user_data")) + _, _, exec_user = self.current_user error = bleach.clean(self.get_argument('error', "WTF Error!")) template = "panel/denied.html" page_data = { - 'user_data': user_data, + 'user_data': exec_user, 'error': error } @@ -164,10 +164,13 @@ class AjaxHandler(BaseHandler): @tornado.web.authenticated def post(self, page): - user_data = json.loads(self.get_secure_cookie("user_data")) + api_key, _, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser + server_id = self.get_argument('id', None) - exec_user_id = user_data['user_id'] - exec_user = helper_users.get_user(exec_user_id) + permissions = { 'Commands': Enum_Permissions_Server.Commands, 'Terminal': Enum_Permissions_Server.Terminal, @@ -178,17 +181,17 @@ class AjaxHandler(BaseHandler): 'Config': Enum_Permissions_Server.Config, 'Players': Enum_Permissions_Server.Players, } - user_perms = self.controller.server_perms.get_server_permissions_foruser(exec_user_id, server_id) + user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id) error = bleach.clean(self.get_argument('error', "WTF Error!")) page_data = { - 'user_data': user_data, + 'user_data': exec_user, 'error': error } if page == "send_command": command = self.get_body_argument('command', default=None, strip=True) - server_id = self.get_argument('id') + server_id = self.get_argument('id', None) if server_id is None: logger.warning("Server ID not found in send_command ajax call") @@ -200,11 +203,11 @@ class AjaxHandler(BaseHandler): if srv_obj.check_running(): srv_obj.send_command(command) - self.controller.management.add_to_audit_log(user_data['user_id'], "Sent command to {} terminal: {}".format(self.controller.servers.get_server_friendly_name(server_id), command), server_id, self.get_remote_ip()) + self.controller.management.add_to_audit_log(exec_user['user_id'], "Sent command to {} terminal: {}".format(self.controller.servers.get_server_friendly_name(server_id), command), server_id, self.get_remote_ip()) elif page == "create_file": if not permissions['Files'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Files") return file_parent = helper.get_os_understandable_path(self.get_body_argument('file_parent', default=None, strip=True)) @@ -227,7 +230,7 @@ class AjaxHandler(BaseHandler): elif page == "create_dir": if not permissions['Files'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Files") return dir_parent = helper.get_os_understandable_path(self.get_body_argument('dir_parent', default=None, strip=True)) @@ -248,7 +251,7 @@ class AjaxHandler(BaseHandler): elif page == "unzip_file": if not permissions['Files'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Files") return server_id = self.get_argument('id', None) @@ -259,7 +262,7 @@ class AjaxHandler(BaseHandler): elif page == "kill": if not permissions['Commands'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Commands") return server_id = self.get_argument('id', None) @@ -272,11 +275,11 @@ class AjaxHandler(BaseHandler): elif page == "eula": server_id = self.get_argument('id', None) svr = self.controller.get_server_obj(server_id) - svr.agree_eula(user_data['user_id']) + svr.agree_eula(exec_user['user_id']) elif page == "restore_backup": if not permissions['Backup'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Backups") return server_id = bleach.clean(self.get_argument('id', None)) @@ -295,16 +298,21 @@ class AjaxHandler(BaseHandler): elif page == "unzip_server": path = self.get_argument('path', None) - helper.unzipServer(path, exec_user_id) + helper.unzipServer(path, exec_user['user_id']) return @tornado.web.authenticated def delete(self, page): - user_data = json.loads(self.get_secure_cookie("user_data")) + api_key, _, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser + server_id = self.get_argument('id', None) - exec_user_id = user_data['user_id'] - exec_user = helper_users.get_user(exec_user_id) + + + permissions = { 'Commands': Enum_Permissions_Server.Commands, 'Terminal': Enum_Permissions_Server.Terminal, @@ -315,10 +323,10 @@ class AjaxHandler(BaseHandler): 'Config': Enum_Permissions_Server.Config, 'Players': Enum_Permissions_Server.Players, } - user_perms = self.controller.server_perms.get_server_permissions_foruser(exec_user_id, server_id) + user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id) if page == "del_file": if not permissions['Files'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Files") return file_path = helper.get_os_understandable_path(self.get_body_argument('file_path', default=None, strip=True)) @@ -350,7 +358,7 @@ class AjaxHandler(BaseHandler): if page == "del_backup": if not permissions['Backup'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Backups") return file_path = helper.get_os_understandable_path(self.get_body_argument('file_path', default=None, strip=True)) @@ -376,7 +384,7 @@ class AjaxHandler(BaseHandler): elif page == "del_dir": if not permissions['Files'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Files") return dir_path = helper.get_os_understandable_path(self.get_body_argument('dir_path', default=None, strip=True)) @@ -401,7 +409,7 @@ class AjaxHandler(BaseHandler): elif page == "delete_server": if not permissions['Config'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Config") return server_id = self.get_argument('id', None) @@ -411,7 +419,7 @@ class AjaxHandler(BaseHandler): elif page == "delete_server_files": if not permissions['Config'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Config") return server_id = self.get_argument('id', None) @@ -421,10 +429,12 @@ class AjaxHandler(BaseHandler): @tornado.web.authenticated def put(self, page): - user_data = json.loads(self.get_secure_cookie("user_data")) + api_key, _, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser + server_id = self.get_argument('id', None) - exec_user_id = user_data['user_id'] - exec_user = helper_users.get_user(exec_user_id) permissions = { 'Commands': Enum_Permissions_Server.Commands, 'Terminal': Enum_Permissions_Server.Terminal, @@ -435,10 +445,10 @@ class AjaxHandler(BaseHandler): 'Config': Enum_Permissions_Server.Config, 'Players': Enum_Permissions_Server.Players, } - user_perms = self.controller.server_perms.get_server_permissions_foruser(exec_user_id, server_id) + user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id) if page == "save_file": if not permissions['Files'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Files") return file_contents = self.get_body_argument('file_contents', default=None, strip=True) @@ -460,7 +470,7 @@ class AjaxHandler(BaseHandler): elif page == "rename_item": if not permissions['Files'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Files") return item_path = helper.get_os_understandable_path(self.get_body_argument('item_path', default=None, strip=True)) diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py index 05f23c95..7e81e475 100644 --- a/app/classes/web/api_handler.py +++ b/app/classes/web/api_handler.py @@ -1,14 +1,11 @@ -import os -import secrets -import threading -import tornado.web -import tornado.escape import logging +import re from app.classes.web.base_handler import BaseHandler log = logging.getLogger(__name__) +bearer_pattern = re.compile(r'^Bearer', flags=re.IGNORECASE) class ApiHandler(BaseHandler): @@ -16,7 +13,7 @@ class ApiHandler(BaseHandler): # Define a standardized response self.set_status(status) self.write(data) - + def access_denied(self, user, reason=''): if reason: reason = ' because ' + reason log.info("User %s from IP %s was denied access to the API route " + self.request.path + reason, user, self.get_remote_ip()) @@ -28,8 +25,14 @@ class ApiHandler(BaseHandler): def authenticate_user(self) -> bool: try: log.debug("Searching for specified token") - # TODO: YEET THIS - user_data = self.controller.users.get_user_by_api_token(self.get_argument('token')) + + api_token = self.get_argument('token', '') + if api_token is None and self.request.headers.get('Authorization'): + api_token = bearer_pattern.sub('', self.request.headers.get('Authorization')) + elif api_token is None: + api_token = self.get_cookie('token') + user_data = self.controller.users.get_user_by_api_token(api_token) + log.debug("Checking results") if user_data: # Login successful! Check perms @@ -40,11 +43,11 @@ class ApiHandler(BaseHandler): else: logging.debug("Auth unsuccessful") self.access_denied("unknown", "the user provided an invalid token") - return + return False except Exception as e: log.warning("An error occured while authenticating an API user: %s", e) self.access_denied("unknown"), "an error occured while authenticating the user" - return + return False class ServersStats(ApiHandler): diff --git a/app/classes/web/base_handler.py b/app/classes/web/base_handler.py index e4046d1e..24310c69 100644 --- a/app/classes/web/base_handler.py +++ b/app/classes/web/base_handler.py @@ -4,10 +4,12 @@ import bleach from typing import ( Union, List, - Optional + Optional, Tuple, Dict, Any ) +from app.classes.shared.authentication import authentication from app.classes.shared.main_controller import Controller +from app.classes.models.users import ApiKeys logger = logging.getLogger(__name__) @@ -17,7 +19,8 @@ class BaseHandler(tornado.web.RequestHandler): nobleach = {bool, type(None)} redactables = ("pass", "api") - def initialize(self, controller : Controller = None, tasks_manager=None, translator=None): + # noinspection PyAttributeOutsideInit + def initialize(self, controller: Controller = None, tasks_manager=None, translator=None): self.controller = controller self.tasks_manager = tasks_manager self.translator = translator @@ -28,8 +31,9 @@ class BaseHandler(tornado.web.RequestHandler): self.request.remote_ip return remote_ip - def get_current_user(self): - return self.get_secure_cookie("user", max_age_days=1) + current_user: Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]] + def get_current_user(self) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]: + return authentication.check(self.get_cookie("token")) def autobleach(self, name, text): for r in self.redactables: diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 07eb96c9..475c17a2 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -1,4 +1,7 @@ from tempfile import tempdir +from typing import Dict, Optional, Any + +from app.classes.shared.authentication import authentication from app.classes.shared.translation import Translation import json import logging @@ -15,8 +18,7 @@ from cron_validator import CronValidator #TZLocal is set as a hidden import on win pipeline from tzlocal import get_localzone -from tornado import locale -from tornado import iostream +from tornado import locale, iostream from tornado.ioloop import IOLoop from app.classes.shared.console import console from app.classes.shared.main_models import Users, installer @@ -36,6 +38,129 @@ logger = logging.getLogger(__name__) class PanelHandler(BaseHandler): + def get_user_roles(self) -> Dict[str, list]: + user_roles = {} + for user in self.controller.users.get_all_users(): + user_roles_list = self.controller.users.get_user_roles_names(user.user_id) + # user_servers = self.controller.servers.get_authorized_servers(user.user_id) + user_roles[user.user_id] = user_roles_list + return user_roles + + def get_role_servers(self) -> set: + servers = set() + for server in self.controller.list_defined_servers(): + argument = int(float( + bleach.clean( + self.get_argument('server_{}_access'.format(server['server_id']), '0') + ) + )) + if argument: + servers.add(server['server_id']) + return servers + + def get_perms_quantity(self) -> (str, dict): + permissions_mask: str = "000" + server_quantity: dict = {} + for permission in self.controller.crafty_perms.list_defined_crafty_permissions(): + argument = int(float(bleach.clean( + self.get_argument('permission_{}'.format(permission.name), '0') + ))) + if argument: + permissions_mask = self.controller.crafty_perms.set_permission(permissions_mask, permission, argument) + + q_argument = int(float( + bleach.clean( + self.get_argument('quantity_{}'.format(permission.name), '0') + ) + )) + if q_argument: + server_quantity[permission.name] = q_argument + else: + server_quantity[permission.name] = 0 + return permissions_mask, server_quantity + + def get_perms(self) -> str: + permissions_mask: str = "000" + for permission in self.controller.crafty_perms.list_defined_crafty_permissions(): + argument = self.get_argument('permission_{}'.format(permission.name), None) + if argument is not None: + permissions_mask = self.controller.crafty_perms.set_permission(permissions_mask, permission, + 1 if argument == '1' else 0) + return permissions_mask + + def get_perms_server(self) -> str: + permissions_mask = "00000000" + for permission in self.controller.server_perms.list_defined_permissions(): + argument = self.get_argument('permission_{}'.format(permission.name), None) + if argument is not None: + permissions_mask = self.controller.server_perms.set_permission(permissions_mask, permission, + 1 if argument == '1' else 0) + return permissions_mask + + def get_user_role_memberships(self) -> set: + roles = set() + for role in self.controller.roles.get_all_roles(): + if self.get_argument('role_{}_membership'.format(role.role_id), None) == '1': + roles.add(role.role_id) + return roles + + def download_file(self, name: str, file: str): + self.set_header('Content-Type', 'application/octet-stream') + self.set_header('Content-Disposition', 'attachment; filename=' + name) + chunk_size = 1024 * 1024 * 4 # 4 MiB + + with open(file, 'rb') as f: + while True: + chunk = f.read(chunk_size) + if not chunk: + break + try: + self.write(chunk) # write the chunk to response + self.flush() # send the chunk to client + except iostream.StreamClosedError: + # this means the client has closed the connection + # so break the loop + break + finally: + # deleting the chunk is very important because + # if many clients are downloading files at the + # same time, the chunks in memory will keep + # increasing and will eat up the RAM + del chunk + + def check_server_id(self) -> Optional[str]: + server_id = self.get_argument('id', None) + + api_key, _, exec_user = self.current_user + superuser = exec_user['superuser'] + + # Commented out because there is no server access control for API keys, they just inherit from the host user + # if api_key is not None: + # superuser = superuser and api_key.superuser + + if server_id is None: + self.redirect("/panel/error?error=Invalid Server ID") + return None + else: + # Does this server exist? + if not self.controller.servers.server_id_exists(server_id): + self.redirect("/panel/error?error=Invalid Server ID") + return None + + # Does the user have permission? + if not superuser: # TODO: Figure out a better solution + if api_key is not None: + if not self.controller.servers.server_id_authorized_api_key(server_id, api_key): + print(f'API key {api_key.name} (id: {api_key.token_id}) does not have permission') + self.redirect("/panel/error?error=Invalid Server ID") + return None + else: + if not self.controller.servers.server_id_authorized(server_id, exec_user["user_id"]): + print(f'User {exec_user["user_id"]} does not have permission') + self.redirect("/panel/error?error=Invalid Server ID") + return None + return server_id + # Server fetching, spawned asynchronously # TODO: Make the related front-end elements update with AJAX def fetch_server_data(self, page_data): @@ -56,36 +181,41 @@ class PanelHandler(BaseHandler): @tornado.web.authenticated async def get(self, page): - error = bleach.clean(self.get_argument('error', "WTF Error!")) + error = self.get_argument('error', "WTF Error!") template = "panel/denied.html" now = time.time() formatted_time = str(datetime.datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')) - exec_user_data = json.loads(self.get_secure_cookie("user_data")) - exec_user_id = exec_user_data['user_id'] - exec_user = self.controller.users.get_user_by_id(exec_user_id) + api_key, token_data, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser exec_user_role = set() - if exec_user['superuser'] == 1: + if superuser: # TODO: Figure out a better solution defined_servers = self.controller.list_defined_servers() exec_user_role.add("Super User") exec_user_crafty_permissions = self.controller.crafty_perms.list_defined_crafty_permissions() else: - exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(exec_user_id) + if api_key is not None: + exec_user_crafty_permissions = self.controller.crafty_perms.get_api_key_permissions_list(api_key) + else: + exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list( + exec_user["user_id"]) logger.debug(exec_user['roles']) for r in exec_user['roles']: role = self.controller.roles.get_role(r) exec_user_role.add(role['role_name']) - defined_servers = self.controller.servers.get_authorized_servers(exec_user_id) + defined_servers = self.controller.servers.get_authorized_servers(exec_user["user_id"]) - page_data = { + page_data: Dict[str, Any] = { # todo: make this actually pull and compare version data 'update_available': False, 'serverTZ':get_localzone(), 'version_data': helper.get_version_string(), - 'user_data': exec_user_data, + 'user_data': exec_user, 'user_role' : exec_user_role, 'user_crafty_permissions' : exec_user_crafty_permissions, 'crafty_permissions': { @@ -102,10 +232,18 @@ class PanelHandler(BaseHandler): 'hosts_data': self.controller.management.get_latest_hosts_stats(), 'show_contribute': helper.get_setting("show_contribute_link", True), 'error': error, - 'time': formatted_time + 'time': formatted_time, + 'lang': self.controller.users.get_user_lang_by_id(exec_user["user_id"]), + 'super_user': superuser, + 'api_key': { + 'name': api_key.name, + 'created': api_key.created, + 'server_permissions': api_key.server_permissions, + 'crafty_permissions': api_key.crafty_permissions, + 'superuser': api_key.superuser + } if api_key is not None else None, + 'superuser': superuser } - page_data['lang'] = self.controller.users.get_user_lang_by_id(exec_user_id) - page_data['super_user'] = exec_user['superuser'] if page == 'unauthorized': template = "panel/denied.html" @@ -115,11 +253,11 @@ class PanelHandler(BaseHandler): elif page == 'credits': with open(helper.credits_cache) as republic_credits_will_do: - credits = json.load(republic_credits_will_do) - timestamp = credits["lastUpdate"] / 1000.0 - page_data["patrons"] = credits["patrons"] - page_data["staff"] = credits["staff"] - page_data["translations"] = credits["translations"] + credits_dict: dict = json.load(republic_credits_will_do) + timestamp = credits_dict["lastUpdate"] / 1000.0 + page_data["patrons"] = credits_dict["patrons"] + page_data["staff"] = credits_dict["staff"] + page_data["translations"] = credits_dict["translations"] page_data["lastUpdate"] = str(datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')) template = "panel/credits.html" @@ -129,7 +267,7 @@ class PanelHandler(BaseHandler): elif page == "remove_server": server_id = self.get_argument('id', None) - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access: not superuser") return elif server_id is None: @@ -139,7 +277,7 @@ class PanelHandler(BaseHandler): server_data = self.controller.get_server_data(server_id) server_name = server_data['server_name'] - self.controller.management.add_to_audit_log(exec_user_data['user_id'], + self.controller.management.add_to_audit_log(exec_user['user_id'], "Deleted server {} named {}".format(server_id, server_name), server_id, self.get_remote_ip()) @@ -149,36 +287,30 @@ class PanelHandler(BaseHandler): return elif page == 'dashboard': - if exec_user['superuser'] == 1: + if superuser: # TODO: Figure out a better solution try: page_data['servers'] = self.controller.servers.get_all_servers_stats() except IndexError: self.controller.stats.record_stats() page_data['servers'] = self.controller.servers.get_all_servers_stats() + else: + try: + user_auth = self.controller.servers.get_authorized_servers_stats(exec_user["user_id"]) + except IndexError: + self.controller.stats.record_stats() + user_auth = self.controller.servers.get_authorized_servers_stats(exec_user["user_id"]) + logger.debug(f"ASFR: {user_auth}") + page_data['servers'] = user_auth + page_data['server_stats']['running'] = len( + list(filter(lambda x: x['stats']['running'], page_data['servers']))) + page_data['server_stats']['stopped'] = len(page_data['servers']) - page_data['server_stats']['running'] for data in page_data['servers']: try: - data['stats']['waiting_start'] = self.controller.servers.get_waiting_start(int(data['stats']['server_id']['server_id'])) - except: - data['stats']['waiting_start'] = False - else: - try: - user_auth = self.controller.servers.get_authorized_servers_stats(exec_user_id) - except IndexError: - self.controller.stats.record_stats() - user_auth = self.controller.servers.get_authorized_servers_stats(exec_user_id) - logger.debug("ASFR: {}".format(user_auth)) - page_data['servers'] = user_auth - page_data['server_stats']['running'] = 0 - page_data['server_stats']['stopped'] = 0 - for data in page_data['servers']: - if data['stats']['running']: - page_data['server_stats']['running'] += 1 - else: - page_data['server_stats']['stopped'] += 1 - try: - page_data['stats']['waiting_start'] = self.controller.servers.get_waiting_start(int(data['stats']['server_id']['server_id'])) - except: + data['stats']['waiting_start'] = self.controller.servers.get_waiting_start( + str(data['stats']['server_id']['server_id'])) + except Exception as e: + logger.error("Failed to get server waiting to start: {} ".format(e)) data['stats']['waiting_start'] = False page_data['num_players'] = 0 @@ -188,23 +320,10 @@ class PanelHandler(BaseHandler): template = "panel/dashboard.html" elif page == 'server_detail': - server_id = self.get_argument('id', None) subpage = bleach.clean(self.get_argument('subpage', "")) - if server_id is None: - self.redirect("/panel/error?error=Invalid Server ID") - return - else: - # does this server id exist? - if not self.controller.servers.server_id_exists(server_id): - self.redirect("/panel/error?error=Invalid Server ID") - return - - if exec_user['superuser'] != 1: - if not self.controller.servers.server_id_authorized(server_id, exec_user_id): - if not self.controller.servers.server_id_authorized(int(server_id), exec_user_id): - self.redirect("/panel/error?error=Invalid Server ID") - return False + server_id = self.check_server_id() + if server_id is None: return valid_subpages = ['term', 'logs', 'backup', 'config', 'files', 'admin_controls', 'tasks'] @@ -219,7 +338,8 @@ class PanelHandler(BaseHandler): page_data['server_stats'] = self.controller.servers.get_server_stats_by_id(server_id) try: page_data['waiting_start'] = self.controller.servers.get_waiting_start(server_id) - except: + except Exception as e: + logger.error("Failed to get server waiting to start: {} ".format(e)) page_data['waiting_start'] = False page_data['get_players'] = lambda: self.controller.stats.get_server_players(server_id) page_data['active_link'] = subpage @@ -233,44 +353,44 @@ class PanelHandler(BaseHandler): 'Config': Enum_Permissions_Server.Config, 'Players': Enum_Permissions_Server.Players, } - page_data['user_permissions'] = self.controller.server_perms.get_server_permissions_foruser(exec_user_id, server_id) + page_data['user_permissions'] = self.controller.server_perms.get_user_id_permissions_list(exec_user["user_id"], server_id) if subpage == 'term': if not page_data['permissions']['Terminal'] in page_data['user_permissions']: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Terminal") return if subpage == 'logs': if not page_data['permissions']['Logs'] in page_data['user_permissions']: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Logs") return if subpage == 'tasks': if not page_data['permissions']['Schedule'] in page_data['user_permissions']: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access To Scheduled Tasks") return page_data['schedules'] = management_helper.get_schedules_by_server(server_id) if subpage == 'config': if not page_data['permissions']['Config'] in page_data['user_permissions']: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access Server Config") return if subpage == 'files': if not page_data['permissions']['Files'] in page_data['user_permissions']: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access Files") return if subpage == "backup": if not page_data['permissions']['Backup'] in page_data['user_permissions']: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Backups") return server_info = self.controller.servers.get_server_data_by_id(server_id) @@ -303,7 +423,7 @@ class PanelHandler(BaseHandler): return html if subpage == "admin_controls": if not page_data['permissions']['Players'] in page_data['user_permissions']: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access") page_data['banned_players'] = get_banned_players_html() @@ -311,23 +431,11 @@ class PanelHandler(BaseHandler): template = "panel/server_{subpage}.html".format(subpage=subpage) elif page == 'download_backup': - server_id = self.get_argument('id', None) file = self.get_argument('file', "") - if server_id is None: - self.redirect("/panel/error?error=Invalid Server ID") - return - else: - # does this server id exist? - if not self.controller.servers.server_id_exists(server_id): - self.redirect("/panel/error?error=Invalid Server ID") - return + server_id = self.check_server_id() + if server_id is None: return - if exec_user['superuser'] != 1: - #if not self.controller.servers.server_id_authorized(server_id, exec_user_id): - if not self.controller.servers.server_id_authorized(int(server_id), exec_user_id): - self.redirect("/panel/error?error=Invalid Server ID") - return server_info = self.controller.servers.get_server_data_by_id(server_id) backup_file = os.path.abspath(os.path.join(helper.get_os_understandable_path(server_info["backup_path"]), file)) @@ -336,49 +444,17 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Invalid path detected") return - self.set_header('Content-Type', 'application/octet-stream') - self.set_header('Content-Disposition', 'attachment; filename=' + file) - chunk_size = 1024 * 1024 * 4 # 4 MiB + self.download_file(file, backup_file) - with open(backup_file, 'rb') as f: - while True: - chunk = f.read(chunk_size) - if not chunk: - break - try: - self.write(chunk) # write the chunk to response - self.flush() # send the chunk to client - except iostream.StreamClosedError: - # this means the client has closed the connection - # so break the loop - break - finally: - # deleting the chunk is very important because - # if many clients are downloading files at the - # same time, the chunks in memory will keep - # increasing and will eat up the RAM - del chunk self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) elif page == 'backup_now': - server_id = self.get_argument('id', None) + server_id = self.check_server_id() + if server_id is None: return - if server_id is None: - self.redirect("/panel/error?error=Invalid Server ID") - return - else: - # does this server id exist? - if not self.controller.servers.server_id_exists(server_id): - self.redirect("/panel/error?error=Invalid Server ID") - return + server = self.controller.get_server_obj(server_id) - if exec_user['superuser'] != 1: - #if not self.controller.servers.server_id_authorized(server_id, exec_user_id): - if not self.controller.servers.server_id_authorized(int(server_id), exec_user_id): - self.redirect("/panel/error?error=Invalid Server ID") - return - - server = self.controller.get_server_obj(server_id).backup_server() + server.backup_server() self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) elif page == 'panel_config': @@ -419,11 +495,10 @@ class PanelHandler(BaseHandler): for user in page_data['users']: if user.user_id != exec_user['user_id']: user.api_token = "********" - if exec_user['superuser']: + if superuser: for user in self.controller.users.get_all_users(): if user.superuser == 1: - super_auth_servers = [] - super_auth_servers.append("Super User Access To All Servers") + super_auth_servers = ["Super User Access To All Servers"] page_data['users'] = self.controller.users.get_all_users() page_data['roles'] = self.controller.roles.get_all_roles() page_data['auth-servers'][user.user_id] = super_auth_servers @@ -438,7 +513,6 @@ class PanelHandler(BaseHandler): page_data['user']['email'] = "" page_data['user']['enabled'] = True page_data['user']['superuser'] = False - page_data['user']['api_token'] = "N/A" page_data['user']['created'] = "N/A" page_data['user']['last_login'] = "N/A" page_data['user']['last_ip'] = "N/A" @@ -457,8 +531,8 @@ class PanelHandler(BaseHandler): page_data['permissions_list'] = set() page_data['quantity_server'] = self.controller.crafty_perms.list_all_crafty_permissions_quantity_limits() page_data['languages'] = [] - page_data['languages'].append(self.controller.users.get_user_lang_by_id(exec_user_id)) - if exec_user['superuser']: + page_data['languages'].append(self.controller.users.get_user_lang_by_id(exec_user["user_id"])) + if superuser: page_data['super-disabled'] = '' else: page_data['super-disabled'] = 'disabled' @@ -483,8 +557,8 @@ class PanelHandler(BaseHandler): 'Config': Enum_Permissions_Server.Config, 'Players': Enum_Permissions_Server.Players, } - page_data['user_permissions'] = self.controller.server_perms.get_server_permissions_foruser(exec_user_id, server_id) - exec_user_server_permissions = self.controller.server_perms.get_user_permissions_list(exec_user_id, server_id) + page_data['user_permissions'] = self.controller.server_perms.get_user_id_permissions_list(exec_user["user_id"], server_id) + exec_user_server_permissions = self.controller.server_perms.get_user_permissions_list(exec_user["user_id"], server_id) page_data['server_data'] = self.controller.servers.get_server_data_by_id(server_id) page_data['server_stats'] = self.controller.servers.get_server_stats_by_id(server_id) page_data['new_schedule'] = True @@ -503,7 +577,7 @@ class PanelHandler(BaseHandler): page_data['schedule']['interval_type'] = 'days' if not Enum_Permissions_Server.Schedule in exec_user_server_permissions: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access To Scheduled Tasks") return @@ -525,8 +599,8 @@ class PanelHandler(BaseHandler): 'Config': Enum_Permissions_Server.Config, 'Players': Enum_Permissions_Server.Players, } - page_data['user_permissions'] = self.controller.server_perms.get_server_permissions_foruser(exec_user_id, server_id) - exec_user_server_permissions = self.controller.server_perms.get_user_permissions_list(exec_user_id, server_id) + page_data['user_permissions'] = self.controller.server_perms.get_user_id_permissions_list(exec_user["user_id"], server_id) + exec_user_server_permissions = self.controller.server_perms.get_user_permissions_list(exec_user["user_id"], server_id) page_data['server_data'] = self.controller.servers.get_server_data_by_id(server_id) page_data['server_stats'] = self.controller.servers.get_server_stats_by_id(server_id) page_data['new_schedule'] = False @@ -555,7 +629,7 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Invalid server ID or Schedule ID") if not Enum_Permissions_Server.Schedule in exec_user_server_permissions: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access To Scheduled Tasks") return @@ -580,7 +654,7 @@ class PanelHandler(BaseHandler): page_data['languages'] = [] page_data['languages'].append(self.controller.users.get_user_lang_by_id(user_id)) #checks if super user. If not we disable the button. - if exec_user['superuser'] and str(exec_user['user_id']) != str(user_id): + if superuser and str(exec_user['user_id']) != str(user_id): page_data['super-disabled'] = '' else: page_data['super-disabled'] = 'disabled' @@ -594,7 +668,7 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Invalid User ID") return elif Enum_Permissions_Crafty.User_Config not in exec_user_crafty_permissions: - if str(user_id) != str(exec_user_id): + if str(user_id) != str(exec_user["user_id"]): self.redirect("/panel/error?error=Unauthorized access: not a user editor") return @@ -610,14 +684,28 @@ class PanelHandler(BaseHandler): page_data['user']['email'] = "" template = "panel/panel_edit_user.html" + elif page == "edit_user_apikeys": + user_id = self.get_argument('id', None) + page_data['user'] = self.controller.users.get_user_by_id(user_id) + page_data['api_keys'] = self.controller.users.get_user_api_keys(user_id) + # self.controller.crafty_perms.list_defined_crafty_permissions() + page_data['server_permissions_all'] = self.controller.server_perms.list_defined_permissions() + page_data['crafty_permissions_all'] = self.controller.crafty_perms.list_defined_crafty_permissions() + + if user_id is None: + self.redirect("/panel/error?error=Invalid User ID") + return + + template = "panel/panel_edit_user_apikeys.html" + elif page == "remove_user": user_id = bleach.clean(self.get_argument('id', None)) - if not exec_user['superuser'] and Enum_Permissions_Crafty.User_Config not in exec_user_crafty_permissions: + if not superuser and Enum_Permissions_Crafty.User_Config not in exec_user_crafty_permissions: self.redirect("/panel/error?error=Unauthorized access: not superuser") return - elif str(exec_user_id) == str(user_id): + elif str(exec_user["user_id"]) == str(user_id): self.redirect("/panel/error?error=Unauthorized access: you cannot delete yourself") return elif user_id is None: @@ -636,18 +724,13 @@ class PanelHandler(BaseHandler): self.controller.users.remove_user(user_id) self.controller.management.add_to_audit_log(exec_user['user_id'], - "Removed user {} (UID:{})".format(target_user['username'], user_id), + f"Removed user {target_user['username']} (UID:{user_id})", server_id=0, source_ip=self.get_remote_ip()) self.redirect("/panel/panel_config") elif page == "add_role": - user_roles = {} - for user in self.controller.users.get_all_users(): - user_roles_list = self.controller.users.get_user_roles_names(user.user_id) - user_servers = self.controller.servers.get_authorized_servers(user.user_id) - data = {user.user_id: user_roles_list} - user_roles.update(data) + user_roles = self.get_user_roles() page_data['new_role'] = True page_data['role'] = {} page_data['role']['role_name'] = "" @@ -668,14 +751,7 @@ class PanelHandler(BaseHandler): template = "panel/panel_edit_role.html" elif page == "edit_role": - auth_servers = {} - - user_roles = {} - for user in self.controller.users.get_all_users(): - user_roles_list = self.controller.users.get_user_roles_names(user.user_id) - user_servers = self.controller.servers.get_authorized_servers(user.user_id) - data = {user.user_id: user_roles_list} - user_roles.update(data) + user_roles = self.get_user_roles() page_data['new_role'] = False role_id = self.get_argument('id', None) page_data['role'] = self.controller.roles.get_role_with_servers(role_id) @@ -697,7 +773,7 @@ class PanelHandler(BaseHandler): elif page == "remove_role": role_id = bleach.clean(self.get_argument('id', None)) - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access: not superuser") return elif role_id is None: @@ -713,7 +789,7 @@ class PanelHandler(BaseHandler): self.controller.roles.remove_role(role_id) self.controller.management.add_to_audit_log(exec_user['user_id'], - "Removed role {} (RID:{})".format(target_role['role_name'], role_id), + f"Removed role {target_role['role_name']} (RID:{role_id})", server_id=0, source_ip=self.get_remote_ip()) self.redirect("/panel/panel_config") @@ -724,23 +800,11 @@ class PanelHandler(BaseHandler): template = "panel/activity_logs.html" elif page == 'download_file': - server_id = self.get_argument('id', None) file = helper.get_os_understandable_path(self.get_argument('path', "")) name = self.get_argument('name', "") - if server_id is None: - self.redirect("/panel/error?error=Invalid Server ID") - return - else: - # does this server id exist? - if not self.controller.servers.server_id_exists(server_id): - self.redirect("/panel/error?error=Invalid Server ID") - return - - if exec_user['superuser'] != 1: - if not self.controller.servers.server_id_authorized(int(server_id), exec_user_id): - self.redirect("/panel/error?error=Invalid Server ID") - return + server_id = self.check_server_id() + if server_id is None: return server_info = self.controller.servers.get_server_data_by_id(server_id) @@ -749,34 +813,13 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Invalid path detected") return - self.set_header('Content-Type', 'application/octet-stream') - self.set_header('Content-Disposition', 'attachment; filename=' + name) - chunk_size = 1024 * 1024 * 4 # 4 MiB - - with open(file, 'rb') as f: - while True: - chunk = f.read(chunk_size) - if not chunk: - break - try: - self.write(chunk) # write the chunk to response - self.flush() # send the chunk to client - except iostream.StreamClosedError: - # this means the client has closed the connection - # so break the loop - break - finally: - # deleting the chunk is very important because - # if many clients are downloading files at the - # same time, the chunks in memory will keep - # increasing and will eat up the RAM - del chunk + self.download_file(name, file) self.redirect("/panel/server_detail?id={}&subpage=files".format(server_id)) elif page == 'download_support_package': tempZipStorage = exec_user['support_logs'] #We'll reset the support path for this user now. - self.controller.users.set_support_path(exec_user_id, "") + self.controller.users.set_support_path(exec_user["user_id"], "") self.set_header('Content-Type', 'application/octet-stream') self.set_header('Content-Disposition', 'attachment; filename=' + "support_logs.zip") @@ -806,7 +849,7 @@ class PanelHandler(BaseHandler): return elif page == "support_logs": - logger.info("Support logs requested. Packinging logs for user with ID: {}".format(exec_user_id)) + logger.info("Support logs requested. Packinging logs for user with ID: {}".format(exec_user["user_id"])) logs_thread = threading.Thread(target=self.controller.package_support_logs, daemon=True, args=(exec_user,), name='{}_logs_thread'.format(exec_user['user_id'])) logs_thread.start() self.redirect('/panel/dashboard') @@ -824,9 +867,11 @@ class PanelHandler(BaseHandler): @tornado.web.authenticated def post(self, page): - exec_user_data = json.loads(self.get_secure_cookie("user_data")) - exec_user_id = exec_user_data['user_id'] - exec_user = self.controller.users.get_user_by_id(exec_user_id) + api_key, token_data, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser + server_id = self.get_argument('id', None) permissions = { 'Commands': Enum_Permissions_Server.Commands, @@ -838,28 +883,27 @@ class PanelHandler(BaseHandler): 'Config': Enum_Permissions_Server.Config, 'Players': Enum_Permissions_Server.Players, } - user_perms = self.controller.server_perms.get_server_permissions_foruser(exec_user_id, server_id) + user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user["user_id"], server_id) exec_user_role = set() - if exec_user['superuser'] == 1: - defined_servers = self.controller.list_defined_servers() + if superuser: + # defined_servers = self.controller.list_defined_servers() exec_user_role.add("Super User") exec_user_crafty_permissions = self.controller.crafty_perms.list_defined_crafty_permissions() else: - exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(exec_user_id) - defined_servers = self.controller.servers.get_authorized_servers(exec_user_id) + exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(exec_user["user_id"]) + # defined_servers = self.controller.servers.get_authorized_servers(exec_user["user_id"]) for r in exec_user['roles']: role = self.controller.roles.get_role(r) exec_user_role.add(role['role_name']) if page == 'server_detail': if not permissions['Config'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access to Config") - return - server_id = self.get_argument('id', None) + return server_name = self.get_argument('server_name', None) server_obj = self.controller.servers.get_server_obj(server_id) - if exec_user['superuser']: + if superuser: server_path = self.get_argument('server_path', None) log_path = self.get_argument('log_path', None) executable = self.get_argument('executable', None) @@ -875,20 +919,11 @@ class PanelHandler(BaseHandler): auto_start = int(float(self.get_argument('auto_start', '0'))) crash_detection = int(float(self.get_argument('crash_detection', '0'))) logs_delete_after = int(float(self.get_argument('logs_delete_after', '0'))) - subpage = self.get_argument('subpage', None) + # TODO: Add more modify options via the subpage parameter + # subpage = self.get_argument('subpage', None) - if not exec_user['superuser']: - if not self.controller.servers.server_id_authorized(server_id, exec_user_id): - self.redirect("/panel/error?error=Unauthorized access: invalid server id") - return - elif server_id is None: - self.redirect("/panel/error?error=Invalid Server ID") - return - else: - # does this server id exist? - if not self.controller.servers.server_id_exists(server_id): - self.redirect("/panel/error?error=Invalid Server ID") - return + server_id = self.check_server_id() + if server_id is None: return server_obj = self.controller.servers.get_server_obj(server_id) server_settings = self.controller.get_server_data(server_id) @@ -898,7 +933,7 @@ class PanelHandler(BaseHandler): execution_command = execution_command.replace(str(stale_executable), str(executable)) server_obj.server_name = server_name - if exec_user['superuser']: + if superuser: if helper.validate_traversal(helper.get_servers_root_dir(), server_path): server_obj.path = server_path server_obj.log_path = log_path @@ -937,14 +972,14 @@ class PanelHandler(BaseHandler): logger.debug(self.request.arguments) server_id = self.get_argument('id', None) server_obj = self.controller.servers.get_server_obj(server_id) - if exec_user['superuser']: + if superuser: backup_path = bleach.clean(self.get_argument('backup_path', None)) else: backup_path = server_obj.backup_path max_backups = bleach.clean(self.get_argument('max_backups', None)) if not permissions['Backup'] in user_perms: - if not exec_user['superuser']: + if not superuser: self.redirect("/panel/error?error=Unauthorized access: User not authorized") return elif server_id is None: @@ -962,7 +997,7 @@ class PanelHandler(BaseHandler): self.controller.management.set_backup_config(server_id, max_backups=max_backups) self.controller.management.add_to_audit_log(exec_user['user_id'], - "Edited server {}: updated backups".format(server_id), + f"Edited server {server_id}: updated backups", server_id, self.get_remote_ip()) self.tasks_manager.reload_schedule_from_db() @@ -1019,7 +1054,7 @@ class PanelHandler(BaseHandler): else: one_time = False - if not exec_user['superuser'] and not permissions['Backup'] in user_perms: + if not superuser and not permissions['Backup'] in user_perms: self.redirect("/panel/error?error=Unauthorized access: User not authorized") return elif server_id is None: @@ -1139,7 +1174,7 @@ class PanelHandler(BaseHandler): else: one_time = False - if not exec_user['superuser'] and not permissions['Backup'] in user_perms: + if not superuser and not permissions['Backup'] in user_perms: self.redirect("/panel/error?error=Unauthorized access: User not authorized") return elif server_id is None: @@ -1217,9 +1252,9 @@ class PanelHandler(BaseHandler): password1 = bleach.clean(self.get_argument('password1', None)) email = bleach.clean(self.get_argument('email', "default@example.com")) enabled = int(float(self.get_argument('enabled', '0'))) - regen_api = int(float(self.get_argument('regen_api', '0'))) lang = bleach.clean(self.get_argument('language'), 'en_EN') - if exec_user['superuser']: + + if superuser: #Checks if user is trying to change super user status of self. We don't want that. Automatically make them stay super user since we know they are. if str(exec_user['user_id']) != str(user_id): superuser = bleach.clean(self.get_argument('superuser', '0')) @@ -1233,7 +1268,7 @@ class PanelHandler(BaseHandler): superuser = False if Enum_Permissions_Crafty.User_Config not in exec_user_crafty_permissions: - if str(user_id) != str(exec_user_id): + if str(user_id) != str(exec_user["user_id"]): self.redirect("/panel/error?error=Unauthorized access: not a user editor") return @@ -1245,8 +1280,7 @@ class PanelHandler(BaseHandler): self.controller.users.update_user(user_id, user_data=user_data) self.controller.management.add_to_audit_log(exec_user['user_id'], - "Edited user {} (UID:{}) password".format(username, - user_id), + f"Edited user {username} (UID:{user_id}) password", server_id=0, source_ip=self.get_remote_ip()) self.redirect("/panel/panel_config") @@ -1267,36 +1301,8 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Passwords must match") return - roles = set() - for role in self.controller.roles.get_all_roles(): - argument = int(float( - bleach.clean( - self.get_argument('role_{}_membership'.format(role.role_id), '0') - ) - )) - if argument: - roles.add(role.role_id) - - permissions_mask = "000" - server_quantity = {} - for permission in self.controller.crafty_perms.list_defined_crafty_permissions(): - argument = int(float( - bleach.clean( - self.get_argument('permission_{}'.format(permission.name), '0') - ) - )) - if argument: - permissions_mask = self.controller.crafty_perms.set_permission(permissions_mask, permission, argument) - - q_argument = int(float( - bleach.clean( - self.get_argument('quantity_{}'.format(permission.name), '0') - ) - )) - if q_argument: - server_quantity[permission.name] = q_argument - else: - server_quantity[permission.name] = 0 + roles = self.get_user_role_memberships() + permissions_mask, server_quantity = self.get_perms_quantity() # if email is None or "": # email = "default@example.com" @@ -1306,7 +1312,6 @@ class PanelHandler(BaseHandler): "password": password0, "email": email, "enabled": enabled, - "regen_api": regen_api, "roles": roles, "lang": lang, "superuser": superuser, @@ -1318,11 +1323,62 @@ class PanelHandler(BaseHandler): self.controller.users.update_user(user_id, user_data=user_data, user_crafty_data=user_crafty_data) self.controller.management.add_to_audit_log(exec_user['user_id'], - "Edited user {} (UID:{}) with roles {} and permissions {}".format(username, user_id, roles, permissions_mask), + f"Edited user {username} (UID:{user_id}) with roles {roles} and permissions {permissions_mask}", server_id=0, source_ip=self.get_remote_ip()) self.redirect("/panel/panel_config") + elif page == "edit_user_apikeys": + user_id = self.get_argument('id', None) + name = self.get_argument('name', None) + superuser = self.get_argument('superuser', None) == '1' + + if name is None or name == "": + self.redirect("/panel/error?error=Invalid API key name") + return + elif user_id is None: + self.redirect("/panel/error?error=Invalid User ID") + return + else: + # does this user id exist? + if not self.controller.users.user_id_exists(user_id): + self.redirect("/panel/error?error=Invalid User ID") + return + + crafty_permissions_mask = self.get_perms() + server_permissions_mask = self.get_perms_server() + + self.controller.users.add_user_api_key(name, user_id, superuser, crafty_permissions_mask, server_permissions_mask) + + self.controller.management.add_to_audit_log(exec_user['user_id'], + f"Added API key {name} with crafty permissions {crafty_permissions_mask} and {server_permissions_mask} for user with UID: {user_id}", + server_id=0, + source_ip=self.get_remote_ip()) + self.redirect(f"/panel/edit_user_apikeys?id={user_id}") + + elif page == "get_token": + key_id = self.get_argument('id', None) + + if key_id is None: + self.redirect("/panel/error?error=Invalid Key ID") + return + else: + key = self.controller.users.get_user_api_key(key_id) + # does this user id exist? + if key is None: + self.redirect("/panel/error?error=Invalid Key ID") + return + + self.controller.management.add_to_audit_log(exec_user['user_id'], + f"Generated a new API token for the key {key.name} from user with UID: {key.user.user_id}", + server_id=0, + source_ip=self.get_remote_ip()) + + self.write(authentication.generate(key.user.user_id, { + 'token_id': key.token_id + })) + self.finish() + elif page == "add_user": if bleach.clean(self.get_argument('username', None)).lower() == 'system': @@ -1334,7 +1390,7 @@ class PanelHandler(BaseHandler): email = bleach.clean(self.get_argument('email', "default@example.com")) enabled = int(float(self.get_argument('enabled', '0'))), lang = bleach.clean(self.get_argument('lang', 'en_EN')) - if exec_user['superuser']: + if superuser: superuser = bleach.clean(self.get_argument('superuser', '0')) else: superuser = '0' @@ -1359,36 +1415,8 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Passwords must match") return - roles = set() - for role in self.controller.roles.get_all_roles(): - argument = int(float( - bleach.clean( - self.get_argument('role_{}_membership'.format(role.role_id), '0') - ) - )) - if argument: - roles.add(role.role_id) - - permissions_mask = "000" - server_quantity = {} - for permission in self.controller.crafty_perms.list_defined_crafty_permissions(): - argument = int(float( - bleach.clean( - self.get_argument('permission_{}'.format(permission.name), '0') - ) - )) - if argument: - permissions_mask = self.controller.crafty_perms.set_permission(permissions_mask, permission, argument) - - q_argument = int(float( - bleach.clean( - self.get_argument('quantity_{}'.format(permission.name), '0') - ) - )) - if q_argument: - server_quantity[permission.name] = q_argument - else: - server_quantity[permission.name] = 0 + roles = self.get_user_role_memberships() + permissions_mask, server_quantity = self.get_perms_quantity() user_id = self.controller.users.add_user(username, password=password0, email=email, enabled=enabled, superuser=superuser) user_data = { @@ -1430,25 +1458,8 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Invalid Role ID") return - servers = set() - for server in self.controller.list_defined_servers(): - argument = int(float( - bleach.clean( - self.get_argument('server_{}_access'.format(server['server_id']), '0') - ) - )) - if argument: - servers.add(server['server_id']) - - permissions_mask = "00000000" - for permission in self.controller.server_perms.list_defined_permissions(): - argument = int(float( - bleach.clean( - self.get_argument('permission_{}'.format(permission.name), '0') - ) - )) - if argument: - permissions_mask = self.controller.server_perms.set_permission(permissions_mask, permission, argument) + servers = self.get_role_servers() + permissions_mask = self.get_perms_server() role_data = { "role_name": role_name, @@ -1478,25 +1489,8 @@ class PanelHandler(BaseHandler): self.redirect("/panel/error?error=Role exists") return - servers = set() - for server in self.controller.list_defined_servers(): - argument = int(float( - bleach.clean( - self.get_argument('server_{}_access'.format(server['server_id']), '0') - ) - )) - if argument: - servers.add(server['server_id']) - - permissions_mask = "00000000" - for permission in self.controller.server_perms.list_defined_permissions(): - argument = int(float( - bleach.clean( - self.get_argument('permission_{}'.format(permission.name), '0') - ) - )) - if argument: - permissions_mask = self.controller.server_perms.set_permission(permissions_mask, permission, argument) + servers = self.get_role_servers() + permissions_mask = self.get_perms_server() role_id = self.controller.roles.add_role(role_name) self.controller.roles.update_role(role_id, {"servers": servers}, permissions_mask) @@ -1513,10 +1507,57 @@ class PanelHandler(BaseHandler): else: self.set_status(404) - page_data = {} - page_data['lang'] = locale.get("en_EN") + page_data = {'lang': locale.get("en_EN")} self.render( "public/404.html", translate=self.translator.translate, data=page_data ) + + @tornado.web.authenticated + def delete(self, page): + api_key, token_data, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser + + page_data = { + # todo: make this actually pull and compare version data + 'update_available': False, + 'version_data': helper.get_version_string(), + 'user_data': exec_user, + 'hosts_data': self.controller.management.get_latest_hosts_stats(), + 'show_contribute': helper.get_setting("show_contribute_link", True), + 'lang': self.controller.users.get_user_lang_by_id(exec_user["user_id"]) + } + + if page == "remove_apikey": + key_id = bleach.clean(self.get_argument('id', None)) + + if not superuser: + self.redirect("/panel/error?error=Unauthorized access: not superuser") + return + elif key_id is None or self.controller.users.get_user_api_key(key_id) is None: + self.redirect("/panel/error?error=Invalid Key ID") + return + else: + # does this user id exist? + target_key = self.controller.users.get_user_api_key(key_id) + if not target_key: + self.redirect("/panel/error?error=Invalid Key ID") + return + + self.controller.users.delete_user_api_key(key_id) + + self.controller.management.add_to_audit_log(exec_user['user_id'], + f"Removed API key {target_key} (ID: {key_id}) from user {exec_user['user_id']}", + server_id=0, + source_ip=self.get_remote_ip()) + self.redirect("/panel/panel_config") + else: + self.set_status(404) + self.render( + "public/404.html", + data=page_data, + translate=self.translator.translate, + ) diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py index c367a62e..6b8cb2de 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -7,6 +7,7 @@ import requests import tornado.web import tornado.escape +from app.classes.shared.authentication import authentication from app.classes.shared.helpers import Helpers, helper from app.classes.web.base_handler import BaseHandler from app.classes.shared.console import console @@ -27,7 +28,7 @@ except ModuleNotFoundError as e: class PublicHandler(BaseHandler): - def set_current_user(self, user): + def set_current_user(self, user_id: str = None): expire_days = helper.get_setting('cookie_expire') @@ -35,8 +36,8 @@ class PublicHandler(BaseHandler): if not expire_days: expire_days = "5" - if user: - self.set_secure_cookie("user", tornado.escape.json_encode(user), expires_days=int(expire_days)) + if user_id is not None: + self.set_cookie("token", authentication.generate(user_id), expires_days=int(expire_days)) else: self.clear_cookie("user") @@ -45,12 +46,7 @@ class PublicHandler(BaseHandler): error = bleach.clean(self.get_argument('error', "Invalid Login!")) error_msg = bleach.clean(self.get_argument('error_msg', '')) - page_data = { - 'version': helper.get_version_string(), - 'error': error - } - - page_data['lang'] = tornado.locale.get("en_EN") + page_data = {'version': helper.get_version_string(), 'error': error, 'lang': tornado.locale.get("en_EN")} # sensible defaults template = "public/404.html" @@ -112,7 +108,7 @@ class PublicHandler(BaseHandler): # Valid Login if login_result: - self.set_current_user(entered_username) + self.set_current_user(user_data.user_id) logger.info("User: {} Logged in from IP: {}".format(user_data, self.get_remote_ip())) # record this login @@ -140,15 +136,6 @@ class PublicHandler(BaseHandler): profile_url = "/static/assets/images/faces-clipart/pic-3.png" else: profile_url = "/static/assets/images/faces-clipart/pic-3.png" - cookie_data = { - "username": user_data.username, - "user_id": user_data.user_id, - "email": user_data.email, - "profile_url": profile_url, - "account_type": user_data.superuser, - } - - self.set_secure_cookie('user_data', json.dumps(cookie_data)) next_page = "/panel/dashboard" self.redirect(next_page) diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index 5ab73993..f027ae04 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -28,13 +28,13 @@ class ServerHandler(BaseHandler): @tornado.web.authenticated def get(self, page): - # name = tornado.escape.json_decode(self.current_user) - exec_user_data = json.loads(self.get_secure_cookie("user_data")) - exec_user_id = exec_user_data['user_id'] - exec_user = self.controller.users.get_user_by_id(exec_user_id) + api_key, token_data, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser exec_user_role = set() - if exec_user['superuser'] == 1: + if superuser: defined_servers = self.controller.list_defined_servers() exec_user_role.add("Super User") exec_user_crafty_permissions = self.controller.crafty_perms.list_defined_crafty_permissions() @@ -42,8 +42,8 @@ class ServerHandler(BaseHandler): for role in self.controller.roles.get_all_roles(): list_roles.append(self.controller.roles.get_role(role.role_id)) else: - exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(exec_user_id) - defined_servers = self.controller.servers.get_authorized_servers(exec_user_id) + exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(exec_user["user_id"]) + defined_servers = self.controller.servers.get_authorized_servers(exec_user["user_id"]) list_roles = [] for r in exec_user['roles']: role = self.controller.roles.get_role(r) @@ -54,7 +54,7 @@ class ServerHandler(BaseHandler): page_data = { 'version_data': helper.get_version_string(), - 'user_data': exec_user_data, + 'user_data': exec_user, 'user_role' : exec_user_role, 'roles' : list_roles, 'user_crafty_permissions' : exec_user_crafty_permissions, @@ -71,13 +71,13 @@ class ServerHandler(BaseHandler): 'hosts_data': self.controller.management.get_latest_hosts_stats(), 'menu_servers': defined_servers, 'show_contribute': helper.get_setting("show_contribute_link", True), - 'lang': self.controller.users.get_user_lang_by_id(exec_user_id) + 'lang': self.controller.users.get_user_lang_by_id(exec_user["user_id"]) } - if exec_user['superuser'] == 1: + if superuser == 1: page_data['roles'] = list_roles if page == "step1": - if not exec_user['superuser'] and not self.controller.crafty_perms.can_create_server(exec_user_id): + if not superuser and not self.controller.crafty_perms.can_create_server(exec_user["user_id"]): self.redirect("/panel/error?error=Unauthorized access: not a server creator or server limit reached") return @@ -93,17 +93,17 @@ class ServerHandler(BaseHandler): @tornado.web.authenticated def post(self, page): - - exec_user_data = json.loads(self.get_secure_cookie("user_data")) - exec_user_id = exec_user_data['user_id'] - exec_user = self.controller.users.get_user_by_id(exec_user_id) + api_key, token_data, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser template = "public/404.html" page_data = { - 'version_data': "version_data_here", - 'user_data': exec_user_data, + 'version_data': "version_data_here", # TODO + 'user_data': exec_user, 'show_contribute': helper.get_setting("show_contribute_link", True), - 'lang': self.controller.users.get_user_lang_by_id(exec_user_id) + 'lang': self.controller.users.get_user_lang_by_id(exec_user["user_id"]) } if page == "command": @@ -151,11 +151,11 @@ class ServerHandler(BaseHandler): return - self.controller.management.send_command(exec_user_data['user_id'], server_id, self.get_remote_ip(), command) + self.controller.management.send_command(exec_user['user_id'], server_id, self.get_remote_ip(), command) if page == "step1": - if not exec_user['superuser']: + if not superuser: user_roles = self.controller.roles.get_all_roles() else: user_roles = self.controller.roles.get_all_roles() @@ -185,7 +185,7 @@ class ServerHandler(BaseHandler): return new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) - self.controller.management.add_to_audit_log(exec_user_data['user_id'], + self.controller.management.add_to_audit_log(exec_user['user_id'], "imported a jar server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative" new_server_id, self.get_remote_ip()) @@ -201,7 +201,7 @@ class ServerHandler(BaseHandler): if new_server_id == "false": self.redirect("/panel/error?error=Zip file not accessible! You can fix this permissions issue with sudo chown -R crafty:crafty {} And sudo chmod 2775 -R {}".format(import_server_path, import_server_path)) return - self.controller.management.add_to_audit_log(exec_user_data['user_id'], + self.controller.management.add_to_audit_log(exec_user['user_id'], "imported a zip server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative" new_server_id, self.get_remote_ip()) @@ -213,21 +213,21 @@ class ServerHandler(BaseHandler): return server_type, server_version = server_parts # TODO: add server type check here and call the correct server add functions if not a jar - role_ids = self.controller.users.get_user_roles_id(exec_user_id) + role_ids = self.controller.users.get_user_roles_id(exec_user["user_id"]) new_server_id = self.controller.create_jar_server(server_type, server_version, server_name, min_mem, max_mem, port) - self.controller.management.add_to_audit_log(exec_user_data['user_id'], + self.controller.management.add_to_audit_log(exec_user['user_id'], "created a {} {} server named \"{}\"".format(server_version, str(server_type).capitalize(), server_name), # Example: Admin created a 1.16.5 Bukkit server named "survival" new_server_id, self.get_remote_ip()) # These lines create a new Role for the Server with full permissions and add the user to it if he's not a superuser if len(captured_roles) == 0: - if not exec_user['superuser']: + if not superuser: new_server_uuid = self.controller.servers.get_server_data_by_id(new_server_id).get("server_uuid") role_id = self.controller.roles.add_role("Creator of Server with uuid={}".format(new_server_uuid)) self.controller.server_perms.add_role_server(new_server_id, role_id, "11111111") - self.controller.users.add_role_to_user(exec_user_id, role_id) - self.controller.crafty_perms.add_server_creation(exec_user_id) + self.controller.users.add_role_to_user(exec_user["user_id"], role_id) + self.controller.crafty_perms.add_server_creation(exec_user["user_id"]) else: for role in captured_roles: diff --git a/app/classes/web/upload_handler.py b/app/classes/web/upload_handler.py index 719df844..0a92a6f0 100644 --- a/app/classes/web/upload_handler.py +++ b/app/classes/web/upload_handler.py @@ -20,6 +20,7 @@ MAX_STREAMED_SIZE = 1024 * 1024 * 1024 @tornado.web.stream_request_body class UploadHandler(tornado.web.RequestHandler): + # noinspection PyAttributeOutsideInit def initialize(self, controller: Controller=None, tasks_manager=None, translator=None): self.controller = controller self.tasks_manager = tasks_manager @@ -27,8 +28,19 @@ class UploadHandler(tornado.web.RequestHandler): def prepare(self): self.do_upload = True - user_data = json.loads(self.get_secure_cookie('user_data')) - user_id = user_data['user_id'] + api_key, token_data, exec_user = self.current_user + superuser = exec_user['superuser'] + if api_key is not None: + superuser = superuser and api_key.superuser + user_id = exec_user['user_id'] + + if superuser: + exec_user_crafty_permissions = self.controller.crafty_perms.list_defined_crafty_permissions() + elif api_key is not None: + exec_user_crafty_permissions = self.controller.crafty_perms.get_api_key_permissions_list(api_key) + else: + exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list( + exec_user["user_id"]) server_id = self.request.headers.get('X-ServerId', None) @@ -42,8 +54,7 @@ class UploadHandler(tornado.web.RequestHandler): console.warning('Server ID not found in upload handler call') self.do_upload = False - user_permissions = self.controller.server_perms.get_user_permissions_list(user_id, server_id) - if Enum_Permissions_Server.Files not in user_permissions: + if Enum_Permissions_Server.Files not in exec_user_crafty_permissions: logger.warning(f'User {user_id} tried to upload a file to {server_id} without permissions!') console.warning(f'User {user_id} tried to upload a file to {server_id} without permissions!') self.do_upload = False diff --git a/app/classes/web/websocket_handler.py b/app/classes/web/websocket_handler.py index 1ec54f3a..1f0c2db5 100644 --- a/app/classes/web/websocket_handler.py +++ b/app/classes/web/websocket_handler.py @@ -5,6 +5,7 @@ import sys from urllib.parse import parse_qsl from app.classes.models.users import Users +from app.classes.shared.authentication import authentication from app.classes.shared.helpers import helper from app.classes.web.websocket_helper import websocket_helper from app.classes.shared.console import console @@ -19,7 +20,14 @@ except ModuleNotFoundError as e: console.critical("Import Error: Unable to load {} module".format(e, e.name)) sys.exit(1) + class SocketHandler(tornado.websocket.WebSocketHandler): + page = None + page_query_params = None + controller = None + tasks_manager = None + translator = None + io_loop = None def initialize(self, controller=None, tasks_manager=None, translator=None): self.controller = controller @@ -34,24 +42,11 @@ class SocketHandler(tornado.websocket.WebSocketHandler): return remote_ip def get_user_id(self): - user_data_cookie_raw = self.get_secure_cookie('user_data') - - if user_data_cookie_raw and user_data_cookie_raw.decode('utf-8'): - user_data_cookie = user_data_cookie_raw.decode('utf-8') - user_id = json.loads(user_data_cookie)['user_id'] - return user_id + _, _, user = authentication.check(self.get_cookie('token')) + return user.user_id def check_auth(self): - user_data_cookie_raw = self.get_secure_cookie('user_data') - - if user_data_cookie_raw and user_data_cookie_raw.decode('utf-8'): - user_data_cookie = user_data_cookie_raw.decode('utf-8') - user_id = json.loads(user_data_cookie)['user_id'] - query = Users.select().where(Users.user_id == user_id) - if query.exists(): - return True - return False - + return authentication.check_bool(self.get_cookie('token')) def open(self): logger.debug('Checking WebSocket authentication') @@ -74,10 +69,11 @@ class SocketHandler(tornado.websocket.WebSocketHandler): logger.debug('Opened WebSocket connection') # websocket_helper.broadcast('notification', 'New client connected') - def on_message(self, rawMessage): + @staticmethod + def on_message(raw_message): - logger.debug('Got message from WebSocket connection {}'.format(rawMessage)) - message = json.loads(rawMessage) + logger.debug('Got message from WebSocket connection {}'.format(raw_message)) + message = json.loads(raw_message) logger.debug('Event Type: {}, Data: {}'.format(message['event'], message['data'])) def on_close(self): diff --git a/app/config/config.json b/app/config/config.json index 2d11724a..f0853518 100644 --- a/app/config/config.json +++ b/app/config/config.json @@ -5,6 +5,7 @@ "language": "en_EN", "cookie_expire": 30, "cookie_secret": "random", + "apikey_secret": "random", "show_errors": true, "history_max_age": 7, "stats_update_frequency": 30, diff --git a/app/frontend/templates/notify.html b/app/frontend/templates/notify.html index 4bb76b6e..f33cbdbe 100644 --- a/app/frontend/templates/notify.html +++ b/app/frontend/templates/notify.html @@ -18,19 +18,22 @@