JWT login and multi API keys!

This commit is contained in:
luukas 2022-01-15 02:23:50 +02:00
parent ea3f36809d
commit 93857f90db
36 changed files with 1254 additions and 731 deletions

View File

@ -18,6 +18,7 @@ from app.classes.shared.server import Server
from app.classes.minecraft.server_props import ServerProps from app.classes.minecraft.server_props import ServerProps
from app.classes.minecraft.serverjars import server_jar_obj from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.minecraft.stats import Stats from app.classes.minecraft.stats import Stats
from app.classes.models.users import ApiKeys
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -70,3 +71,7 @@ class Crafty_Perms_Controller:
@staticmethod @staticmethod
def add_server_creation(user_id): def add_server_creation(user_id):
return crafty_permissions.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)

View File

@ -31,10 +31,6 @@ class Management_Controller:
def get_latest_hosts_stats(): def get_latest_hosts_stats():
return management_helper.get_latest_hosts_stats() return management_helper.get_latest_hosts_stats()
@staticmethod
def new_api_token():
return management_helper.new_api_token()
#************************************************************************************************ #************************************************************************************************
# Commands Methods # Commands Methods
#************************************************************************************************ #************************************************************************************************

View File

@ -39,7 +39,9 @@ class Roles_Controller:
@staticmethod @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) base_data = Roles_Controller.get_role_with_servers(role_id)
up_data = {} up_data = {}
added_servers = set() added_servers = set()

View File

@ -14,7 +14,7 @@ from app.classes.shared.console import console
from app.classes.shared.main_models import db_helper 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.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.roles import roles_helper
from app.classes.models.servers import servers_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) permissions_list = server_permissions.get_role_permissions_list(role_id)
return permissions_list 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 @staticmethod
def add_role_server(server_id, role_id, rs_permissions="00000000"): def add_role_server(server_id, role_id, rs_permissions="00000000"):
return server_permissions.add_role_server(server_id, role_id, rs_permissions) 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) return server_permissions.get_role_permissions_list(role_id)
@staticmethod @staticmethod
def 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_permissions_list(user_id, server_id) 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 @staticmethod
def get_authorized_servers_stats_from_roles(user_id): def get_authorized_servers_stats_from_roles(user_id):

View File

@ -17,7 +17,7 @@ from app.classes.shared.console import console
from app.classes.shared.main_models import db_helper from app.classes.shared.main_models import db_helper
from app.classes.models.servers import servers_helper from app.classes.models.servers import servers_helper
from app.classes.models.roles import roles_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.models.server_permissions import server_permissions, Enum_Permissions_Server
from app.classes.shared.server import Server from app.classes.shared.server import Server
@ -82,18 +82,42 @@ class Servers_Controller:
return servers_helper.get_all_servers_stats() return servers_helper.get_all_servers_stats()
@staticmethod @staticmethod
def get_authorized_servers_stats(user_id): def get_authorized_servers_stats_api_key(api_key: ApiKeys):
server_data = [] 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: for s in authorized_servers:
latest = servers_helper.get_latest_server_stats(s.get('server_id')) 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: if Enum_Permissions_Server.Commands in user_permissions:
user_command_permission = True user_command_permission = True
else: else:
user_command_permission = False 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 return server_data
@staticmethod @staticmethod
@ -112,17 +136,21 @@ class Servers_Controller:
return servers_helper.server_id_exists(server_id) return servers_helper.server_id_exists(server_id)
@staticmethod @staticmethod
def server_id_authorized(serverId, user_id): def server_id_authorized(server_id_a, user_id):
authorized = 0 print("Server id authorized: ")
user_roles = users_helper.user_role_query(user_id) user_roles = users_helper.user_role_query(user_id)
for role in user_roles: 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) @staticmethod
def server_id_authorized_api_key(server_id: str, api_key: ApiKeys) -> bool:
if authorized.count() == 0: # TODO
return False return Servers_Controller.server_id_authorized(server_id, api_key.user.user_id)
return True # There is no view server permission
# permission_helper.both_have_perm(api_key)
@staticmethod @staticmethod
def set_update(server_id, value): def set_update(server_id, value):

View File

@ -2,6 +2,8 @@ import os
import time import time
import logging import logging
import sys import sys
from typing import Optional
import yaml import yaml
import asyncio import asyncio
import shutil import shutil
@ -13,6 +15,7 @@ from app.classes.shared.helpers import helper
from app.classes.shared.console import console from app.classes.shared.console import console
from app.classes.models.users import Users, users_helper 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.crafty_permissions import crafty_permissions, Enum_Permissions_Crafty
from app.classes.models.management import management_helper from app.classes.models.management import management_helper
@ -31,10 +34,6 @@ class Users_Controller:
def get_id_by_name(username): def get_id_by_name(username):
return users_helper.get_user_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 @staticmethod
def get_user_lang_by_id(user_id): def get_user_lang_by_id(user_id):
return users_helper.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) users_helper.set_support_path(user_id, support_path)
@staticmethod @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) base_data = users_helper.get_user(user_id)
up_data = {} up_data = {}
added_roles = set() added_roles = set()
@ -64,9 +67,6 @@ class Users_Controller:
elif key == "roles": elif key == "roles":
added_roles = user_data['roles'].difference(base_data['roles']) added_roles = user_data['roles'].difference(base_data['roles'])
removed_roles = base_data['roles'].difference(user_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": elif key == "password":
if user_data['password'] is not None and user_data['password'] != "": if user_data['password'] is not None and user_data['password'] != "":
up_data['password'] = helper.encode_pass(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)) logger.debug("user: {} +role:{} -role:{}".format(user_data, added_roles, removed_roles))
for role in added_roles: for role in added_roles:
users_helper.get_or_create(user_id=user_id, role_id=role) 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_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] limit_role_creation = user_crafty_data['server_quantity'][Enum_Permissions_Crafty.Roles_Config.name]
else: else:
@ -98,8 +97,8 @@ class Users_Controller:
users_helper.update_user(user_id, up_data) users_helper.update_user(user_id, up_data)
@staticmethod @staticmethod
def add_user(username, password=None, email="default@example.com", api_token=None, enabled=True, superuser=False): 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, api_token=api_token, enabled=enabled, superuser=superuser) return users_helper.add_user(username, password=password, email=email, enabled=enabled, superuser=superuser)
@staticmethod @staticmethod
def remove_user(user_id): def remove_user(user_id):
@ -109,9 +108,19 @@ class Users_Controller:
def user_id_exists(user_id): def user_id_exists(user_id):
return users_helper.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 # User Roles Methods
#************************************************************************************************ # ************************************************************************************************
@staticmethod @staticmethod
def get_user_roles_id(user_id): def get_user_roles_id(user_id):
@ -132,3 +141,29 @@ class Users_Controller:
@staticmethod @staticmethod
def user_role_query(user_id): def user_role_query(user_id):
return users_helper.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)

View File

@ -5,7 +5,8 @@ import datetime
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
from app.classes.shared.console import console 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__) logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee') peewee_logger = logging.getLogger('peewee')
@ -191,4 +192,18 @@ class Permissions_Crafty:
User_Crafty.save(user_crafty) User_Crafty.save(user_crafty)
return user_crafty.created_server 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() crafty_permissions = Permissions_Crafty()

View File

@ -7,7 +7,8 @@ from app.classes.shared.helpers import helper
from app.classes.shared.console import console from app.classes.shared.console import console
from app.classes.models.servers import Servers from app.classes.models.servers import Servers
from app.classes.models.roles import Roles 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__) logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee') peewee_logger = logging.getLogger('peewee')
@ -78,10 +79,7 @@ class Permissions_Servers:
@staticmethod @staticmethod
def has_permission(permission_mask, permission_tested: Enum_Permissions_Server): def has_permission(permission_mask, permission_tested: Enum_Permissions_Server):
result = False return permission_mask[permission_tested.value] == '1'
if permission_mask[permission_tested.value] == '1':
result = True
return result
@staticmethod @staticmethod
def set_permission(permission_mask, permission_tested: Enum_Permissions_Server, value): 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): def get_permission(permission_mask, permission_tested: Enum_Permissions_Server):
return permission_mask[permission_tested.value] 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 # Role_Servers Methods
@ -146,7 +152,9 @@ class Permissions_Servers:
Role_Servers.save(role_server) Role_Servers.save(role_server)
@staticmethod @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() return Role_Servers.delete().where(Role_Servers.role_id == role_id).where(Role_Servers.server_id.in_(removed_servers)).execute()
@staticmethod @staticmethod
@ -155,21 +163,52 @@ class Permissions_Servers:
return Role_Servers.delete().where(Role_Servers.server_id == server_id).execute() return Role_Servers.delete().where(Role_Servers.server_id == server_id).execute()
@staticmethod @staticmethod
def get_user_permissions_list(user_id, server_id): def get_user_id_permissions_mask(user_id, server_id: str):
permissions_mask = '' user = users_helper.get_user_model(user_id)
permissions_list = [] return server_permissions.get_user_permissions_mask(user, server_id)
user = users_helper.get_user(user_id) @staticmethod
if user['superuser'] == True: 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() permissions_list = server_permissions.get_permissions_list()
else: else:
roles_list = users_helper.get_user_roles_id(user_id) permissions_mask = server_permissions.get_user_permissions_mask(user, server_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_list = server_permissions.get_permissions(permissions_mask) permissions_list = server_permissions.get_permissions(permissions_mask)
return permissions_list return permissions_list
server_permissions = Permissions_Servers() @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()

View File

@ -2,6 +2,7 @@ import os
import sys import sys
import logging import logging
import datetime import datetime
from typing import Optional, List, Union
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
from app.classes.shared.console import console from app.classes.shared.console import console
@ -41,14 +42,32 @@ class Users(Model):
email = CharField(default="default@example.com") email = CharField(default="default@example.com")
enabled = BooleanField(default=True) enabled = BooleanField(default=True)
superuser = BooleanField(default=False) superuser = BooleanField(default=False)
api_token = CharField(default="", unique=True, index=True) # we may need to revisit this
lang = CharField(default="en_EN") lang = CharField(default="en_EN")
support_logs = CharField(default = '') support_logs = CharField(default = '')
valid_tokens_from = DateTimeField(default=datetime.datetime.now)
class Meta: class Meta:
table_name = "users" table_name = "users"
database = database 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 # User Roles Class
#************************************************************************************************ #************************************************************************************************
@ -86,18 +105,6 @@ class helper_users:
except DoesNotExist: except DoesNotExist:
return None 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 @staticmethod
def user_query(user_id): def user_query(user_id):
user_query = Users.select().where(Users.user_id == user_id) user_query = Users.select().where(Users.user_id == user_id)
@ -117,7 +124,6 @@ class helper_users:
'email': "default@example.com", 'email': "default@example.com",
'enabled': True, 'enabled': True,
'superuser': True, 'superuser': True,
'api_token': None,
'roles': [], 'roles': [],
'servers': [], 'servers': [],
'support_logs': '', 'support_logs': '',
@ -140,21 +146,21 @@ class helper_users:
return False return False
@staticmethod @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: if password is not None:
pw_enc = helper.encode_pass(password) pw_enc = helper.encode_pass(password)
else: else:
pw_enc = None 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({ user_id = Users.insert({
Users.username: username.lower(), Users.username: username.lower(),
Users.password: pw_enc, Users.password: pw_enc,
Users.email: email, Users.email: email,
Users.api_token: api_token,
Users.enabled: enabled, Users.enabled: enabled,
Users.superuser: superuser, Users.superuser: superuser,
Users.created: helper.get_time_as_string() Users.created: helper.get_time_as_string()
@ -162,7 +168,9 @@ class helper_users:
return user_id return user_id
@staticmethod @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: if up_data:
Users.update(up_data).where(Users.user_id == user_id).execute() Users.update(up_data).where(Users.user_id == user_id).execute()
@ -183,14 +191,6 @@ class helper_users:
return False return False
return True 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 # User_Roles Methods
#************************************************************************************************ #************************************************************************************************
@ -223,7 +223,7 @@ class helper_users:
}).execute() }).execute()
@staticmethod @staticmethod
def add_user_roles(user): def add_user_roles(user: Union[dict, Users]):
if type(user) == dict: if type(user) == dict:
user_id = user['user_id'] user_id = user['user_id']
else: else:
@ -237,7 +237,11 @@ class helper_users:
for r in roles_query: for r in roles_query:
roles.add(r.role_id.role_id) 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)) #logger.debug("user: ({}) {}".format(user_id, user))
return user return user
@ -257,5 +261,36 @@ class helper_users:
def remove_roles_from_role_id(role_id): def remove_roles_from_role_id(role_id):
User_Roles.delete().where(User_Roles.role_id == role_id).execute() 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() users_helper = helper_users()

View File

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

View File

@ -41,29 +41,33 @@ class MainPrompt(cmd.Cmd, object):
self.universal_exit() self.universal_exit()
def do_migrations(self, line): def do_migrations(self, line):
if (line == 'up'): if line == 'up':
self.migration_manager.up() self.migration_manager.up()
elif (line == 'down'): elif line == 'down':
self.migration_manager.down() self.migration_manager.down()
elif (line == 'done'): elif line == 'done':
console.info(self.migration_manager.done) console.info(self.migration_manager.done)
elif (line == 'todo'): elif line == 'todo':
console.info(self.migration_manager.todo) console.info(self.migration_manager.todo)
elif (line == 'diff'): elif line == 'diff':
console.info(self.migration_manager.diff) console.info(self.migration_manager.diff)
elif (line == 'info'): elif line == 'info':
console.info('Done: {}'.format(self.migration_manager.done)) console.info('Done: {}'.format(self.migration_manager.done))
console.info('FS: {}'.format(self.migration_manager.todo)) console.info('FS: {}'.format(self.migration_manager.todo))
console.info('Todo: {}'.format(self.migration_manager.diff)) console.info('Todo: {}'.format(self.migration_manager.diff))
elif (line.startswith('add ')): elif line.startswith('add '):
migration_name = line[len('add '):] migration_name = line[len('add '):]
self.migration_manager.create(migration_name, False) self.migration_manager.create(migration_name, False)
else: else:
console.info('Unknown migration command') console.info('Unknown migration command')
def do_threads(self, line): @staticmethod
def do_threads(_line):
for thread in threading.enumerate(): 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): def universal_exit(self):
logger.info("Stopping all server daemons / threads") logger.info("Stopping all server daemons / threads")
@ -75,11 +79,10 @@ class MainPrompt(cmd.Cmd, object):
sys.exit(0) sys.exit(0)
time.sleep(1) time.sleep(1)
@staticmethod @staticmethod
def help_exit(): def help_exit():
console.help("Stops the server if running, Exits the program") console.help("Stops the server if running, Exits the program")
@staticmethod @staticmethod
def help_migrations(): def help_migrations():
console.help("Only for advanced users. Use with caution") console.help("Only for advanced users. Use with caution")

View File

@ -164,25 +164,6 @@ class Helpers:
cmd_out[ci] += c cmd_out[ci] += c
return cmd_out 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): def get_setting(self, key, default_return=False):
try: try:

View File

@ -3,6 +3,8 @@ import pathlib
import time import time
import logging import logging
import sys import sys
from typing import Union
from app.classes.models.server_permissions import Enum_Permissions_Server from app.classes.models.server_permissions import Enum_Permissions_Server
from app.classes.models.users import helper_users from app.classes.models.users import helper_users
from peewee import DoesNotExist 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.helpers import helper
from app.classes.shared.console import console from app.classes.shared.console import console
#Importing Models # Importing Models
from app.classes.models.crafty_permissions import crafty_permissions, Enum_Permissions_Crafty
from app.classes.models.servers import servers_helper 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.crafty_perms_controller import Crafty_Perms_Controller
from app.classes.controllers.management_controller import Management_Controller from app.classes.controllers.management_controller import Management_Controller
from app.classes.controllers.users_controller import Users_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.server_perms_controller import Server_Perms_Controller
from app.classes.controllers.servers_controller import Servers_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__) logger = logging.getLogger(__name__)
class Controller: class Controller:
@ -173,7 +176,7 @@ class Controller:
@staticmethod @staticmethod
def add_system_user(): 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): def get_server_settings(self, server_id):
for s in self.servers_list: 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)) logger.warning("Unable to find server object for server id {}".format(server_id))
return False 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: 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'] return s['server_obj']
logger.warning("Unable to find server object for server id {}".format(server_id)) 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: 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'] return s['server_data_obj']
logger.warning("Unable to find server object for server id {}".format(server_id)) 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: for s in self.servers_list:
# if this is the droid... im mean server we are looking for... # 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_data = self.get_server_data(server_id)
server_name = server_data['server_name'] server_name = server_data['server_name']
backup_dir = self.servers.get_server_data_by_id(server_id)['backup_path'] backup_dir = self.servers.get_server_data_by_id(server_id)['backup_path']

View File

@ -10,6 +10,10 @@ from app.classes.minecraft.server_props import ServerProps
from app.classes.web.websocket_helper import websocket_helper 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__) logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee') peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO) peewee_logger.setLevel(logging.INFO)
@ -39,20 +43,16 @@ class db_builder:
username = default_data.get("username", 'admin') username = default_data.get("username", 'admin')
password = default_data.get("password", 'crafty') password = default_data.get("password", 'crafty')
#api_token = helper.random_string_generator(32)
# #
#Users.insert({ #Users.insert({
# Users.username: username.lower(), # Users.username: username.lower(),
# Users.password: helper.encode_pass(password), # Users.password: helper.encode_pass(password),
# Users.api_token: api_token,
# Users.enabled: True, # Users.enabled: True,
# Users.superuser: True # Users.superuser: True
#}).execute() #}).execute()
user_id = users_helper.add_user(username=username, password=password, email="default@example.com", superuser=True) 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]} ) #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 @staticmethod
def is_fresh_install(): def is_fresh_install():
try: try:

View File

@ -4,13 +4,9 @@ import typing as t
import sys import sys
import os import os
import re import re
from importlib import import_module
from functools import wraps from functools import wraps
try: from functools import cached_property
from functools import cached_property
except ImportError:
from cached_property import cached_property
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
from app.classes.shared.console import console from app.classes.shared.console import console
@ -21,7 +17,7 @@ try:
import peewee import peewee
from playhouse.migrate import ( from playhouse.migrate import (
SchemaMigrator as ScM, SchemaMigrator as ScM,
SqliteMigrator as SqM, SqliteMigrator,
Operation, SQL, operation, SqliteDatabase, Operation, SQL, operation, SqliteDatabase,
make_index_name, Context make_index_name, Context
) )
@ -32,6 +28,22 @@ except ModuleNotFoundError as e:
console.critical("Import Error: Unable to load {} module".format(e.name)) console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1) 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): class MigrateHistory(peewee.Model):
""" """
@ -41,30 +53,15 @@ class MigrateHistory(peewee.Model):
name = peewee.CharField(unique=True) name = peewee.CharField(unique=True)
migrated_at = peewee.DateTimeField(default=datetime.utcnow) migrated_at = peewee.DateTimeField(default=datetime.utcnow)
# noinspection PyTypeChecker
def __unicode__(self) -> str: def __unicode__(self) -> str:
""" """
String representation of this migration String representation of this migration
""" """
return self.name return self.name
class Meta:
MIGRATE_TABLE = 'migratehistory' table_name = MIGRATE_TABLE
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
def get_model(method): def get_model(method):
@ -75,11 +72,12 @@ def get_model(method):
@wraps(method) @wraps(method)
def wrapper(migrator, model, *args, **kwargs): def wrapper(migrator, model, *args, **kwargs):
if isinstance(model, str): 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 method(migrator, model, *args, **kwargs)
return wrapper return wrapper
# noinspection PyProtectedMember
class Migrator(object): class Migrator(object):
def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]): def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]):
""" """
@ -88,8 +86,8 @@ class Migrator(object):
if isinstance(database, peewee.Proxy): if isinstance(database, peewee.Proxy):
database = database.obj database = database.obj
self.database: SqliteDatabase = database self.database: SqliteDatabase = database
self.orm: t.Dict[str, peewee.Model] = {} self.table_dict: t.Dict[str, peewee.Model] = {}
self.operations: t.List[Operation] = [] self.operations: t.List[t.Union[Operation, callable]] = []
self.migrator = SqliteMigrator(database) self.migrator = SqliteMigrator(database)
def run(self): def run(self):
@ -113,13 +111,13 @@ class Migrator(object):
""" """
Executes raw SQL. 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: def create_table(self, model: peewee.Model) -> peewee.Model:
""" """
Creates model and table in database. 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 model._meta.database = self.database
self.operations.append(model.create_table) self.operations.append(model.create_table)
return model return model
@ -129,8 +127,8 @@ class Migrator(object):
""" """
Drops model and table from database. Drops model and table from database.
""" """
del self.orm[model._meta.table_name] del self.table_dict[model._meta.table_name]
self.operations.append(self.migrator.drop_table(model)) self.operations.append(lambda: model.drop_table(cascade=False))
@get_model @get_model
def add_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model: def add_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model:
@ -147,64 +145,16 @@ class Migrator(object):
return model return model
@get_model @get_model
def change_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model: def drop_columns(self, model: peewee.Model, names: str) -> 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:
""" """
Removes fields from model. Removes fields from model.
""" """
fields = [field for field in model._meta.fields.values() fields = [field for field in model._meta.fields.values()
if field.name in names] if field.name in names]
cascade = kwargs.pop('cascade', True)
for field in fields: for field in fields:
self.__del_field__(model, field) self.__del_field__(model, field)
if field.unique: if field.unique:
# Drop unique index
index_name = make_index_name( index_name = make_index_name(
model._meta.table_name, [field.column_name]) model._meta.table_name, [field.column_name])
self.operations.append(self.migrator.drop_index( self.operations.append(self.migrator.drop_index(
@ -250,16 +200,15 @@ class Migrator(object):
Renames table in database. Renames table in database.
""" """
old_name = model._meta.table_name 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 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)) self.operations.append(self.migrator.rename_table(old_name, new_name))
return model return model
@get_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.""" """Create indexes."""
unique = kwargs.pop('unique', False)
model._meta.indexes.append((columns, unique)) model._meta.indexes.append((columns, unique))
columns_ = [] columns_ = []
for col in columns: for col in columns:
@ -329,42 +278,8 @@ class Migrator(object):
return model return model
class SqliteMigrator(SqM): # noinspection PyProtectedMember
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
class MigrationManager(object): class MigrationManager(object):
filemask = re.compile(r"[\d]+_[^\.]+\.py$") filemask = re.compile(r"[\d]+_[^\.]+\.py$")
def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]): def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]):
@ -376,7 +291,7 @@ class MigrationManager(object):
self.database = database self.database = database
@cached_property @cached_property
def model(self) -> peewee.Model: def model(self) -> t.Type[MigrateHistory]:
""" """
Initialize and cache the MigrationHistory model. Initialize and cache the MigrationHistory model.
""" """
@ -487,7 +402,7 @@ class MigrationManager(object):
scope = {} scope = {}
code = compile(code, '<string>', 'exec', dont_inherit=True) code = compile(code, '<string>', 'exec', dont_inherit=True)
exec(code, scope, None) 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, def up_one(self, name: str, migrator: Migrator,
fake: bool = False, rollback: bool = False) -> str: fake: bool = False, rollback: bool = False) -> str:
@ -518,11 +433,11 @@ class MigrationManager(object):
except Exception: except Exception:
self.database.rollback() self.database.rollback()
operation = 'Rollback' if rollback else 'Migration' operation_name = 'Rollback' if rollback else 'Migration'
logger.exception('{} failed: {}'.format(operation, name)) logger.exception('{} failed: {}'.format(operation_name, name))
raise raise
def down(self, name: t.Optional[str] = None): def down(self):
""" """
Rolls back migrations. Rolls back migrations.
""" """

View File

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

View File

@ -35,13 +35,13 @@ class AjaxHandler(BaseHandler):
@tornado.web.authenticated @tornado.web.authenticated
def get(self, page): 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!")) error = bleach.clean(self.get_argument('error', "WTF Error!"))
template = "panel/denied.html" template = "panel/denied.html"
page_data = { page_data = {
'user_data': user_data, 'user_data': exec_user,
'error': error 'error': error
} }
@ -164,10 +164,13 @@ class AjaxHandler(BaseHandler):
@tornado.web.authenticated @tornado.web.authenticated
def post(self, page): 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) server_id = self.get_argument('id', None)
exec_user_id = user_data['user_id']
exec_user = helper_users.get_user(exec_user_id)
permissions = { permissions = {
'Commands': Enum_Permissions_Server.Commands, 'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal, 'Terminal': Enum_Permissions_Server.Terminal,
@ -178,17 +181,17 @@ class AjaxHandler(BaseHandler):
'Config': Enum_Permissions_Server.Config, 'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players, '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!")) error = bleach.clean(self.get_argument('error', "WTF Error!"))
page_data = { page_data = {
'user_data': user_data, 'user_data': exec_user,
'error': error 'error': error
} }
if page == "send_command": if page == "send_command":
command = self.get_body_argument('command', default=None, strip=True) 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: if server_id is None:
logger.warning("Server ID not found in send_command ajax call") logger.warning("Server ID not found in send_command ajax call")
@ -200,11 +203,11 @@ class AjaxHandler(BaseHandler):
if srv_obj.check_running(): if srv_obj.check_running():
srv_obj.send_command(command) 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": elif page == "create_file":
if not permissions['Files'] in user_perms: if not permissions['Files'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files") self.redirect("/panel/error?error=Unauthorized access to Files")
return return
file_parent = helper.get_os_understandable_path(self.get_body_argument('file_parent', default=None, strip=True)) 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": elif page == "create_dir":
if not permissions['Files'] in user_perms: if not permissions['Files'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files") self.redirect("/panel/error?error=Unauthorized access to Files")
return return
dir_parent = helper.get_os_understandable_path(self.get_body_argument('dir_parent', default=None, strip=True)) 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": elif page == "unzip_file":
if not permissions['Files'] in user_perms: if not permissions['Files'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files") self.redirect("/panel/error?error=Unauthorized access to Files")
return return
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
@ -259,7 +262,7 @@ class AjaxHandler(BaseHandler):
elif page == "kill": elif page == "kill":
if not permissions['Commands'] in user_perms: if not permissions['Commands'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Commands") self.redirect("/panel/error?error=Unauthorized access to Commands")
return return
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
@ -272,11 +275,11 @@ class AjaxHandler(BaseHandler):
elif page == "eula": elif page == "eula":
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
svr = self.controller.get_server_obj(server_id) 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": elif page == "restore_backup":
if not permissions['Backup'] in user_perms: if not permissions['Backup'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups") self.redirect("/panel/error?error=Unauthorized access to Backups")
return return
server_id = bleach.clean(self.get_argument('id', None)) server_id = bleach.clean(self.get_argument('id', None))
@ -295,16 +298,21 @@ class AjaxHandler(BaseHandler):
elif page == "unzip_server": elif page == "unzip_server":
path = self.get_argument('path', None) path = self.get_argument('path', None)
helper.unzipServer(path, exec_user_id) helper.unzipServer(path, exec_user['user_id'])
return return
@tornado.web.authenticated @tornado.web.authenticated
def delete(self, page): 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) server_id = self.get_argument('id', None)
exec_user_id = user_data['user_id']
exec_user = helper_users.get_user(exec_user_id)
permissions = { permissions = {
'Commands': Enum_Permissions_Server.Commands, 'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal, 'Terminal': Enum_Permissions_Server.Terminal,
@ -315,10 +323,10 @@ class AjaxHandler(BaseHandler):
'Config': Enum_Permissions_Server.Config, 'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players, '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 page == "del_file":
if not permissions['Files'] in user_perms: if not permissions['Files'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files") self.redirect("/panel/error?error=Unauthorized access to Files")
return return
file_path = helper.get_os_understandable_path(self.get_body_argument('file_path', default=None, strip=True)) 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 page == "del_backup":
if not permissions['Backup'] in user_perms: if not permissions['Backup'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups") self.redirect("/panel/error?error=Unauthorized access to Backups")
return return
file_path = helper.get_os_understandable_path(self.get_body_argument('file_path', default=None, strip=True)) 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": elif page == "del_dir":
if not permissions['Files'] in user_perms: if not permissions['Files'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files") self.redirect("/panel/error?error=Unauthorized access to Files")
return return
dir_path = helper.get_os_understandable_path(self.get_body_argument('dir_path', default=None, strip=True)) 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": elif page == "delete_server":
if not permissions['Config'] in user_perms: if not permissions['Config'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config") self.redirect("/panel/error?error=Unauthorized access to Config")
return return
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
@ -411,7 +419,7 @@ class AjaxHandler(BaseHandler):
elif page == "delete_server_files": elif page == "delete_server_files":
if not permissions['Config'] in user_perms: if not permissions['Config'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config") self.redirect("/panel/error?error=Unauthorized access to Config")
return return
server_id = self.get_argument('id', None) server_id = self.get_argument('id', None)
@ -421,10 +429,12 @@ class AjaxHandler(BaseHandler):
@tornado.web.authenticated @tornado.web.authenticated
def put(self, page): 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) server_id = self.get_argument('id', None)
exec_user_id = user_data['user_id']
exec_user = helper_users.get_user(exec_user_id)
permissions = { permissions = {
'Commands': Enum_Permissions_Server.Commands, 'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal, 'Terminal': Enum_Permissions_Server.Terminal,
@ -435,10 +445,10 @@ class AjaxHandler(BaseHandler):
'Config': Enum_Permissions_Server.Config, 'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players, '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 page == "save_file":
if not permissions['Files'] in user_perms: if not permissions['Files'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files") self.redirect("/panel/error?error=Unauthorized access to Files")
return return
file_contents = self.get_body_argument('file_contents', default=None, strip=True) file_contents = self.get_body_argument('file_contents', default=None, strip=True)
@ -460,7 +470,7 @@ class AjaxHandler(BaseHandler):
elif page == "rename_item": elif page == "rename_item":
if not permissions['Files'] in user_perms: if not permissions['Files'] in user_perms:
if not exec_user['superuser']: if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files") self.redirect("/panel/error?error=Unauthorized access to Files")
return return
item_path = helper.get_os_understandable_path(self.get_body_argument('item_path', default=None, strip=True)) item_path = helper.get_os_understandable_path(self.get_body_argument('item_path', default=None, strip=True))

View File

@ -1,14 +1,11 @@
import os
import secrets
import threading
import tornado.web
import tornado.escape
import logging import logging
import re
from app.classes.web.base_handler import BaseHandler from app.classes.web.base_handler import BaseHandler
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
bearer_pattern = re.compile(r'^Bearer', flags=re.IGNORECASE)
class ApiHandler(BaseHandler): class ApiHandler(BaseHandler):
@ -16,7 +13,7 @@ class ApiHandler(BaseHandler):
# Define a standardized response # Define a standardized response
self.set_status(status) self.set_status(status)
self.write(data) self.write(data)
def access_denied(self, user, reason=''): def access_denied(self, user, reason=''):
if reason: reason = ' because ' + 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()) 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: def authenticate_user(self) -> bool:
try: try:
log.debug("Searching for specified token") 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") log.debug("Checking results")
if user_data: if user_data:
# Login successful! Check perms # Login successful! Check perms
@ -40,11 +43,11 @@ class ApiHandler(BaseHandler):
else: else:
logging.debug("Auth unsuccessful") logging.debug("Auth unsuccessful")
self.access_denied("unknown", "the user provided an invalid token") self.access_denied("unknown", "the user provided an invalid token")
return return False
except Exception as e: except Exception as e:
log.warning("An error occured while authenticating an API user: %s", e) log.warning("An error occured while authenticating an API user: %s", e)
self.access_denied("unknown"), "an error occured while authenticating the user" self.access_denied("unknown"), "an error occured while authenticating the user"
return return False
class ServersStats(ApiHandler): class ServersStats(ApiHandler):

View File

@ -4,10 +4,12 @@ import bleach
from typing import ( from typing import (
Union, Union,
List, List,
Optional Optional, Tuple, Dict, Any
) )
from app.classes.shared.authentication import authentication
from app.classes.shared.main_controller import Controller from app.classes.shared.main_controller import Controller
from app.classes.models.users import ApiKeys
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,7 +19,8 @@ class BaseHandler(tornado.web.RequestHandler):
nobleach = {bool, type(None)} nobleach = {bool, type(None)}
redactables = ("pass", "api") 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.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
@ -28,8 +31,9 @@ class BaseHandler(tornado.web.RequestHandler):
self.request.remote_ip self.request.remote_ip
return remote_ip return remote_ip
def get_current_user(self): current_user: Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]
return self.get_secure_cookie("user", max_age_days=1) 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): def autobleach(self, name, text):
for r in self.redactables: for r in self.redactables:

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import requests
import tornado.web import tornado.web
import tornado.escape import tornado.escape
from app.classes.shared.authentication import authentication
from app.classes.shared.helpers import Helpers, helper from app.classes.shared.helpers import Helpers, helper
from app.classes.web.base_handler import BaseHandler from app.classes.web.base_handler import BaseHandler
from app.classes.shared.console import console from app.classes.shared.console import console
@ -27,7 +28,7 @@ except ModuleNotFoundError as e:
class PublicHandler(BaseHandler): 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') expire_days = helper.get_setting('cookie_expire')
@ -35,8 +36,8 @@ class PublicHandler(BaseHandler):
if not expire_days: if not expire_days:
expire_days = "5" expire_days = "5"
if user: if user_id is not None:
self.set_secure_cookie("user", tornado.escape.json_encode(user), expires_days=int(expire_days)) self.set_cookie("token", authentication.generate(user_id), expires_days=int(expire_days))
else: else:
self.clear_cookie("user") self.clear_cookie("user")
@ -45,12 +46,7 @@ class PublicHandler(BaseHandler):
error = bleach.clean(self.get_argument('error', "Invalid Login!")) error = bleach.clean(self.get_argument('error', "Invalid Login!"))
error_msg = bleach.clean(self.get_argument('error_msg', '')) error_msg = bleach.clean(self.get_argument('error_msg', ''))
page_data = { page_data = {'version': helper.get_version_string(), 'error': error, 'lang': tornado.locale.get("en_EN")}
'version': helper.get_version_string(),
'error': error
}
page_data['lang'] = tornado.locale.get("en_EN")
# sensible defaults # sensible defaults
template = "public/404.html" template = "public/404.html"
@ -112,7 +108,7 @@ class PublicHandler(BaseHandler):
# Valid Login # Valid Login
if login_result: 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())) logger.info("User: {} Logged in from IP: {}".format(user_data, self.get_remote_ip()))
# record this login # record this login
@ -140,15 +136,6 @@ class PublicHandler(BaseHandler):
profile_url = "/static/assets/images/faces-clipart/pic-3.png" profile_url = "/static/assets/images/faces-clipart/pic-3.png"
else: else:
profile_url = "/static/assets/images/faces-clipart/pic-3.png" 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" next_page = "/panel/dashboard"
self.redirect(next_page) self.redirect(next_page)

View File

@ -28,13 +28,13 @@ class ServerHandler(BaseHandler):
@tornado.web.authenticated @tornado.web.authenticated
def get(self, page): def get(self, page):
# name = tornado.escape.json_decode(self.current_user) api_key, token_data, exec_user = self.current_user
exec_user_data = json.loads(self.get_secure_cookie("user_data")) superuser = exec_user['superuser']
exec_user_id = exec_user_data['user_id'] if api_key is not None:
exec_user = self.controller.users.get_user_by_id(exec_user_id) superuser = superuser and api_key.superuser
exec_user_role = set() exec_user_role = set()
if exec_user['superuser'] == 1: if superuser:
defined_servers = self.controller.list_defined_servers() defined_servers = self.controller.list_defined_servers()
exec_user_role.add("Super User") exec_user_role.add("Super User")
exec_user_crafty_permissions = self.controller.crafty_perms.list_defined_crafty_permissions() 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(): for role in self.controller.roles.get_all_roles():
list_roles.append(self.controller.roles.get_role(role.role_id)) list_roles.append(self.controller.roles.get_role(role.role_id))
else: else:
exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(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_id) defined_servers = self.controller.servers.get_authorized_servers(exec_user["user_id"])
list_roles = [] list_roles = []
for r in exec_user['roles']: for r in exec_user['roles']:
role = self.controller.roles.get_role(r) role = self.controller.roles.get_role(r)
@ -54,7 +54,7 @@ class ServerHandler(BaseHandler):
page_data = { page_data = {
'version_data': helper.get_version_string(), 'version_data': helper.get_version_string(),
'user_data': exec_user_data, 'user_data': exec_user,
'user_role' : exec_user_role, 'user_role' : exec_user_role,
'roles' : list_roles, 'roles' : list_roles,
'user_crafty_permissions' : exec_user_crafty_permissions, 'user_crafty_permissions' : exec_user_crafty_permissions,
@ -71,13 +71,13 @@ class ServerHandler(BaseHandler):
'hosts_data': self.controller.management.get_latest_hosts_stats(), 'hosts_data': self.controller.management.get_latest_hosts_stats(),
'menu_servers': defined_servers, 'menu_servers': defined_servers,
'show_contribute': helper.get_setting("show_contribute_link", True), '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 page_data['roles'] = list_roles
if page == "step1": 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") self.redirect("/panel/error?error=Unauthorized access: not a server creator or server limit reached")
return return
@ -93,17 +93,17 @@ class ServerHandler(BaseHandler):
@tornado.web.authenticated @tornado.web.authenticated
def post(self, page): def post(self, page):
api_key, token_data, exec_user = self.current_user
exec_user_data = json.loads(self.get_secure_cookie("user_data")) superuser = exec_user['superuser']
exec_user_id = exec_user_data['user_id'] if api_key is not None:
exec_user = self.controller.users.get_user_by_id(exec_user_id) superuser = superuser and api_key.superuser
template = "public/404.html" template = "public/404.html"
page_data = { page_data = {
'version_data': "version_data_here", 'version_data': "version_data_here", # TODO
'user_data': exec_user_data, 'user_data': exec_user,
'show_contribute': helper.get_setting("show_contribute_link", True), '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": if page == "command":
@ -151,11 +151,11 @@ class ServerHandler(BaseHandler):
return 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 page == "step1":
if not exec_user['superuser']: if not superuser:
user_roles = self.controller.roles.get_all_roles() user_roles = self.controller.roles.get_all_roles()
else: else:
user_roles = self.controller.roles.get_all_roles() user_roles = self.controller.roles.get_all_roles()
@ -185,7 +185,7 @@ class ServerHandler(BaseHandler):
return return
new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) 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" "imported a jar server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative"
new_server_id, new_server_id,
self.get_remote_ip()) self.get_remote_ip())
@ -201,7 +201,7 @@ class ServerHandler(BaseHandler):
if new_server_id == "false": 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)) 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 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" "imported a zip server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative"
new_server_id, new_server_id,
self.get_remote_ip()) self.get_remote_ip())
@ -213,21 +213,21 @@ class ServerHandler(BaseHandler):
return return
server_type, server_version = server_parts server_type, server_version = server_parts
# TODO: add server type check here and call the correct server add functions if not a jar # 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) 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" "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, new_server_id,
self.get_remote_ip()) 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 # 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 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") 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)) 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.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.users.add_role_to_user(exec_user["user_id"], role_id)
self.controller.crafty_perms.add_server_creation(exec_user_id) self.controller.crafty_perms.add_server_creation(exec_user["user_id"])
else: else:
for role in captured_roles: for role in captured_roles:

View File

@ -20,6 +20,7 @@ MAX_STREAMED_SIZE = 1024 * 1024 * 1024
@tornado.web.stream_request_body @tornado.web.stream_request_body
class UploadHandler(tornado.web.RequestHandler): class UploadHandler(tornado.web.RequestHandler):
# noinspection PyAttributeOutsideInit
def initialize(self, controller: Controller=None, tasks_manager=None, translator=None): def initialize(self, controller: Controller=None, tasks_manager=None, translator=None):
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
@ -27,8 +28,19 @@ class UploadHandler(tornado.web.RequestHandler):
def prepare(self): def prepare(self):
self.do_upload = True self.do_upload = True
user_data = json.loads(self.get_secure_cookie('user_data')) api_key, token_data, exec_user = self.current_user
user_id = user_data['user_id'] 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) 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') console.warning('Server ID not found in upload handler call')
self.do_upload = False 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 exec_user_crafty_permissions:
if Enum_Permissions_Server.Files not in user_permissions:
logger.warning(f'User {user_id} tried to upload a file to {server_id} without 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!') console.warning(f'User {user_id} tried to upload a file to {server_id} without permissions!')
self.do_upload = False self.do_upload = False

View File

@ -5,6 +5,7 @@ import sys
from urllib.parse import parse_qsl from urllib.parse import parse_qsl
from app.classes.models.users import Users from app.classes.models.users import Users
from app.classes.shared.authentication import authentication
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
from app.classes.web.websocket_helper import websocket_helper from app.classes.web.websocket_helper import websocket_helper
from app.classes.shared.console import console 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)) console.critical("Import Error: Unable to load {} module".format(e, e.name))
sys.exit(1) sys.exit(1)
class SocketHandler(tornado.websocket.WebSocketHandler): 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): def initialize(self, controller=None, tasks_manager=None, translator=None):
self.controller = controller self.controller = controller
@ -34,24 +42,11 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
return remote_ip return remote_ip
def get_user_id(self): def get_user_id(self):
user_data_cookie_raw = self.get_secure_cookie('user_data') _, _, user = authentication.check(self.get_cookie('token'))
return user.user_id
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
def check_auth(self): def check_auth(self):
user_data_cookie_raw = self.get_secure_cookie('user_data') return authentication.check_bool(self.get_cookie('token'))
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
def open(self): def open(self):
logger.debug('Checking WebSocket authentication') logger.debug('Checking WebSocket authentication')
@ -74,10 +69,11 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
logger.debug('Opened WebSocket connection') logger.debug('Opened WebSocket connection')
# websocket_helper.broadcast('notification', 'New client connected') # 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)) logger.debug('Got message from WebSocket connection {}'.format(raw_message))
message = json.loads(rawMessage) message = json.loads(raw_message)
logger.debug('Event Type: {}, Data: {}'.format(message['event'], message['data'])) logger.debug('Event Type: {}, Data: {}'.format(message['event'], message['data']))
def on_close(self): def on_close(self):

View File

@ -5,6 +5,7 @@
"language": "en_EN", "language": "en_EN",
"cookie_expire": 30, "cookie_expire": 30,
"cookie_secret": "random", "cookie_secret": "random",
"apikey_secret": "random",
"show_errors": true, "show_errors": true,
"history_max_age": 7, "history_max_age": 7,
"stats_update_frequency": 30, "stats_update_frequency": 30,

View File

@ -18,19 +18,22 @@
<li class="nav-item dropdown user-dropdown"> <li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
<img class="img-xs rounded-circle profile-picture" src="{{ data['user_data']['profile_url'] }}" alt="Profile image"> </a> <img class="img-xs rounded-circle profile-picture" src="{{ data['user_data'].get('profile_url') }}" alt="Profile image"> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown"> <div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center"> <div class="dropdown-header text-center">
<img class="img-md rounded-circle profile-picture" src="{{ data['user_data']['profile_url'] }}" alt="Profile image"> <img class="img-md rounded-circle profile-picture" src="{{ data['user_data'].get('profile_url') }}" alt="Profile image">
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p> <p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p> <p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %} {% for r in data['user_role'] %}
<p class="font-weight-light text-muted mb-0">{{ r }}</p> <p class="font-weight-light text-muted mb-0">{{ r }}</p>
{% end %} {% end %}
{% if data['api_key'] %}
<p class="mt-3">Logged in as API key "{{ data['api_key']['name'] }}"</p>
{% end %}
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p> <p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
</div> </div>
<a class="dropdown-item" href="/panel/support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i> Support Logs</i></a> <a class="dropdown-item" href="/panel/support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i> Support Logs</i></a>
{% if "Super User" in data['user_role'] %} {% if data['superuser'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i> Activity</a> <a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i> Activity</a>
{% end %} {% end %}
<a class="dropdown-item" href="/public/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>Sign Out</a> <a class="dropdown-item" href="/public/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>Sign Out</a>

View File

@ -42,7 +42,6 @@
<tr class="rounded"> <tr class="rounded">
<th>User</th> <th>User</th>
<th>Enabled</th> <th>Enabled</th>
<th>API Token</th>
<th>Allowed Servers</th> <th>Allowed Servers</th>
<th>Assigned Roles</th> <th>Assigned Roles</th>
<th>Edit</th> <th>Edit</th>
@ -64,9 +63,6 @@
{% end %} {% end %}
</td> </td>
<td>
<button data-toggle="tooltip" title="Show API Key" data-id="{{ user.api_token }}" type="button" class="btn btn-info show_button">Show</button>
</td>
<td id="server_list_{{user.user_id}}"> <td id="server_list_{{user.user_id}}">
<ul id="{{user.user_id}}"> <ul id="{{user.user_id}}">
{% for item in data['auth-servers'][user.user_id] %} {% for item in data['auth-servers'][user.user_id] %}

View File

@ -151,7 +151,7 @@
<div class="card-header header-sm d-flex justify-content-between align-items-center"> <div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-users"></i> Users Assigned to Role:</h4> <h4 class="card-title"><i class="fas fa-users"></i> Users Assigned to Role:</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
@ -229,7 +229,6 @@
$( document ).ready(function() { $( document ).ready(function() {
console.log( "ready!" ); console.log( "ready!" );
}); });

View File

@ -44,6 +44,10 @@
<i class="fas fa-cogs"></i>Config</a> <i class="fas fa-cogs"></i>Config</a>
</li> </li>
{% if not data['new_user'] %} {% if not data['new_user'] %}
<li class="nav-item">
<a class="nav-link" href="/panel/edit_user_apikeys?id={{ data['user']['user_id'] }}" role="tab" aria-selected="false">
<i class="fas fa-key"></i>API Keys</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/panel/add_user?id={{ data['user']['user_id'] }}&subpage=other" role="tab" aria-selected="false"> <a class="nav-link" href="/panel/add_user?id={{ data['user']['user_id'] }}&subpage=other" role="tab" aria-selected="false">
<i class="fas fa-folder-tree"></i>Other</a> <i class="fas fa-folder-tree"></i>Other</a>
@ -177,14 +181,6 @@
{% end %} {% end %}
</label> </label>
<label for="regen_api" class="form-check-label ml-4 mb-4">
{% if data['new_user'] %}
<input type="checkbox" class="form-check-input" id="regen_api" name="regen_api" checked="" value="1" disabled >Regenerate API Key
{% else %}
<input type="checkbox" class="form-check-input" id="regen_api" name="regen_api" value="1">Regenerate API Key
{% end %}
</label>
<label for="superuser" class="form-check-label ml-4 mb-4"> <label for="superuser" class="form-check-label ml-4 mb-4">
{% if data['user']['superuser'] %} {% if data['user']['superuser'] %}
<input type="checkbox" onclick="superConfirm()" class="form-check-input" id="superuser" name="superuser" checked="" value="1" {{ data['super-disabled'] }} >Super User <input type="checkbox" onclick="superConfirm()" class="form-check-input" id="superuser" name="superuser" checked="" value="1" {{ data['super-disabled'] }} >Super User
@ -215,7 +211,7 @@
<br /> <br />
Last IP: {{ data['user']['last_ip'] }} Last IP: {{ data['user']['last_ip'] }}
<br /> <br />
API Key: {{ data['user']['api_token'] }} API Key: TODO! <!-- TODO -->
<br /> <br />
</p> </p>
</blockquote> </blockquote>

View File

@ -0,0 +1,254 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Edit User API Keys{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
Edit User API Keys - {{ data['user']['user_id'] }}
<br/>
<small>UID: {{ data['user']['user_id'] }}</small>
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item">
<a class="nav-link" href="/panel/edit_user?id={{ data['user']['user_id'] }}&subpage=config"
role="tab"
aria-selected="false">
<i class="fas fa-cogs"></i>Config</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/panel/edit_user_apikeys?id={{ data['user']['user_id'] }}"
role="tab"
aria-selected="true">
<i class="fas fa-key"></i>API Keys</a>
</li>
</ul>
<div class="row">
<div class="col-md-7 col-sm-12">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-key"></i> API Keys</h4>
</div>
<div class="card-body">
<div class="form-group">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr class="rounded">
<!--<th>ID</th>-->
<th>Name</th>
<th>Created</th>
<th>Superuser</th>
<th>Permissions</th>
<th>Buttons</th>
</tr>
</thead>
<tbody>
{% for apikey in data['api_keys'] %}
<tr>
<!--<td>{-{ apikey.token_id }-}</td>-->
<td>{{ apikey.name }}</td>
<td>{{ apikey.created.strftime('%d/%m/%Y %H:%M:%S') }}</td>
<td>
{% if apikey.superuser %}
<span class="text-success">
<i class="fas fa-check-square"></i> Yes
</span>
{% else %}
<span class="text-danger">
<i class="far fa-times-square"></i> No
</span>
{% end %}
</td>
<td>Server: {{ apikey.server_permissions }}
Crafty: {{ apikey.crafty_permissions }}</td>
<td>
<button
class="btn btn-danger delete-api-key"
data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}"
>{{ translate('panelConfig', 'delete', data['lang']) }}</button>
<button
class="btn btn-outline-primary get-a-token"
data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}"
>Get a token
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-5 col-sm-12">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-plus"></i> Create new API key</h4>
</div>
<div class="card-body">
<form id="user_form" class="forms-sample" method="post"
action="/panel/edit_user_apikeys">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['user']['user_id'] }}">
<div class="form-group">
<label class="form-label" for="username">Name <small
class="text-muted ml-1"> - What you wish to
call this API key</small> </label>
<input type="text" class="form-control" name="name" id="name"
placeholder="API Key">
</div>
<table class="table table-hover mb-3">
<thead>
<tr class="rounded">
<th>Permission Name</th>
<th>Authorized ?</th>
</tr>
</thead>
<tbody>
{% for permission in data['server_permissions_all'] %}
<tr>
<td><label
for="permission_{{ permission.name }}">{{ permission.name }}</label>
</td>
<td>
<input type="checkbox" class=""
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
</td>
</tr>
{% end %}
{% for permission in data['crafty_permissions_all'] %}
<tr>
<td><label
for="permission_{{ permission.name }}">{{ permission.name }}</label>
</td>
<td>
<input type="checkbox" class=""
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
</td>
</tr>
{% end %}
</tbody>
</table>
<label for="superuser">Superuser</label>
<input type="checkbox" class="" id="superuser"
name="superuser" value="1">
<br/>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-plus"></i>
Create
</button>
<button type="reset" class="btn btn-light"><i
class="fas fa-undo-alt"></i> {{ translate('panelConfig', 'cancel', data['lang']) }}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
$(document).ready(function () {
console.log("ready!");
$('.delete-api-key').click(function () {
var keyId = $(this).data("key-id");
var keyName = $(this).data("key-name");
bootbox.confirm({
title: `Remove API key ${keyName}?`,
message: "Do you want to delete this API key? This cannot be undone.",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("panelConfig", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
}
},
callback: function (result) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: {'X-XSRFToken': token},
url: '/panel/remove_apikey?id=' + keyId,
success: function (data) {
location.reload();
},
});
}
});
})
$('.get-a-token').click(function () {
var keyId = $(this).data("key-id");
var keyName = $(this).data("key-name");
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/panel/get_token?id=' + keyId,
success: function (data) {
bootbox.alert({
title: `API token for ${keyName}`,
message: `Here is an API token for ${keyName}:\n<pre style="white-space: pre-wrap;color:white;word-break:break-all;background: grey;border-radius: 5px;">${data}</pre>`
});
},
});
})
});
</script>
{% end %}

View File

@ -0,0 +1,12 @@
import peewee
import datetime
def migrate(migrator, database, **kwargs):
migrator.add_columns('users', valid_tokens_from=peewee.DateTimeField(default=datetime.datetime.now))
migrator.drop_columns('users', ['api_token'])
def rollback(migrator, database, **kwargs):
migrator.drop_columns('users', ['valid_tokens_from'])
migrator.add_columns('users', api_token=peewee.CharField(default="", unique=True, index=True))

View File

@ -0,0 +1,23 @@
import peewee
import datetime
from app.classes.models.users import Users
def migrate(migrator, db):
class ApiKeys(peewee.Model):
token_id = peewee.AutoField()
name = peewee.CharField(default='', unique=True, index=True)
created = peewee.DateTimeField(default=datetime.datetime.now)
user = peewee.ForeignKeyField(Users, backref='api_token', index=True)
server_permissions = peewee.CharField(default='00000000')
crafty_permissions = peewee.CharField(default='000')
superuser = peewee.BooleanField(default=False)
class Meta:
table_name = 'api_keys'
migrator.create_table(ApiKeys)
def rollback(migrator, db):
migrator.drop_table('api_keys')

View File

@ -370,5 +370,10 @@
}, },
"base": { "base": {
"doesNotWorkWithoutJavascript": "<strong>Warning: </strong>Crafty doesn't work properly when JavaScript isn't enabled!" "doesNotWorkWithoutJavascript": "<strong>Warning: </strong>Crafty doesn't work properly when JavaScript isn't enabled!"
},
"apiKeys": {
"deleteKeyConfirmation": "Do you want to delete this API key? This cannot be undone.",
"deleteKeyConfirmationTitle": "Remove API key ${keyId}?"
} }
} }

View File

@ -371,5 +371,9 @@
}, },
"base": { "base": {
"doesNotWorkWithoutJavascript": "<strong>Varoitus: </strong>Crafty ei toimi kunnolla ilman JavaScriptiä!" "doesNotWorkWithoutJavascript": "<strong>Varoitus: </strong>Crafty ei toimi kunnolla ilman JavaScriptiä!"
},
"apiKeys": {
"deleteKeyConfirmation": "Haluatko varmasti poistaa tämän API-avaimen? Tämä on peruuttamaton toimenpide!",
"deleteKeyConfirmationTitle": "Poistetaanko API-avain ${keyId}?"
} }
} }

View File

@ -346,5 +346,9 @@
}, },
"base": { "base": {
"doesNotWorkWithoutJavascript": "<strong>Attention: </strong>Crafty ne fonctionne pas correctement si JavaScript n'est pas activé !" "doesNotWorkWithoutJavascript": "<strong>Attention: </strong>Crafty ne fonctionne pas correctement si JavaScript n'est pas activé !"
},
"apiKeys": {
"deleteKeyConfirmation": "Es-tu sûr de vouloir supprimer cette clé API? Tu ne pourras plus revenir en arrière.",
"deleteKeyConfirmationTitle": "Supprimer la clé API ${keyId}?"
} }
} }

View File

@ -15,4 +15,5 @@ tornado~=6.0
cached_property==1.5.2 cached_property==1.5.2
apscheduler~=3.8.1 apscheduler~=3.8.1
cron-validator~=1.0.3 cron-validator~=1.0.3
tzlocal~=4.1 tzlocal~=4.1
pyjwt~=2.3