Merge branch 'bugfix/api-v2-bugfixes' into 'dev'

API v2 bug fixes

See merge request crafty-controller/crafty-4!267
This commit is contained in:
Iain Powrie 2022-05-24 22:42:41 +00:00
commit 54c81d6dd4
20 changed files with 237 additions and 128 deletions

View File

@ -443,7 +443,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
ignored-modules=jsonschema,orjson
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.

View File

@ -11,6 +11,11 @@ logger = logging.getLogger(__name__)
class UsersController:
class ApiPermissionDict(t.TypedDict):
name: str
quantity: int
enabled: bool
def __init__(self, helper, users_helper, authentication):
self.helper = helper
self.users_helper = users_helper

View File

@ -1,4 +1,5 @@
import logging
import typing as t
from enum import Enum
from peewee import (
ForeignKeyField,
@ -99,7 +100,7 @@ class PermissionsCrafty:
try:
user_crafty = UserCrafty.get(UserCrafty.user_id == user_id)
except DoesNotExist:
user_crafty = UserCrafty.insert(
UserCrafty.insert(
{
UserCrafty.user_id: user_id,
UserCrafty.permissions: "000",
@ -114,6 +115,13 @@ class PermissionsCrafty:
user_crafty = PermissionsCrafty.get_user_crafty(user_id)
return user_crafty
@staticmethod
def get_user_crafty_optional(user_id) -> t.Optional[UserCrafty]:
try:
return UserCrafty.get(UserCrafty.user_id == user_id)
except DoesNotExist:
return None
@staticmethod
def add_user_crafty(user_id, uc_permissions):
user_crafty = UserCrafty.insert(

View File

@ -66,10 +66,9 @@ class HelperRoles:
@staticmethod
def get_role_column(role_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Roles, column_name)
return model_to_dict(
Roles.select(column).where(Roles.role_id == role_id).get(),
only=[column],
)[column_name]
return getattr(
Roles.select(column).where(Roles.role_id == role_id).get(), column_name
)
@staticmethod
def add_role(role_name):

View File

@ -205,7 +205,7 @@ class PermissionsServers:
@staticmethod
def get_user_permissions_mask(user: Users, server_id: str):
if user.superuser:
permissions_mask = "1" * len(PermissionsServers.get_permissions_list())
permissions_mask = "1" * len(EnumPermissionsServer)
else:
roles_list = HelperUsers.get_user_roles_id(user.user_id)
role_server = (
@ -217,7 +217,7 @@ class PermissionsServers:
try:
permissions_mask = role_server[0].permissions
except IndexError:
permissions_mask = "0" * len(PermissionsServers.get_permissions_list())
permissions_mask = "0" * len(EnumPermissionsServer)
return permissions_mask
@staticmethod

View File

@ -143,10 +143,10 @@ class HelperServers:
@staticmethod
def get_server_column(server_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Servers, column_name)
return model_to_dict(
return getattr(
Servers.select(column).where(Servers.server_id == server_id).get(),
only=[column],
)[column_name]
column_name,
)
# **********************************************************************************
# Servers Methods

View File

@ -165,19 +165,10 @@ class HelperUsers:
@staticmethod
def get_user_column(user_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Users, column_name)
return model_to_dict(
return getattr(
Users.select(column).where(Users.user_id == user_id).get(),
only=[column],
)[column_name]
@staticmethod
def check_system_user(user_id):
try:
result = Users.get(Users.user_id == user_id).user_id == user_id
if result:
return True
except:
return False
column_name,
)
@staticmethod
def get_user_model(user_id: str) -> Users:

View File

@ -147,10 +147,7 @@ class Controller:
@staticmethod
def check_system_user():
if HelperUsers.get_user_id_by_name("system") is not None:
return True
else:
return False
return HelperUsers.get_user_id_by_name("system") is not None
def set_project_root(self, root_dir):
self.project_root = root_dir
@ -393,12 +390,12 @@ class Controller:
+ ("" if empty else f"\nserver-port={port}")
)
server_file = "server.jar" # HACK: Throw this horrible default out of here
root_create_data = data[data["create_type"] + "_create_data"]
create_data = root_create_data[root_create_data["create_type"] + "_create_data"]
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
server_file = f"{create_data['type']}-{create_data['version']}.jar"
full_jar_path = os.path.join(new_server_path, server_file)
# Create an EULA file
with open(
@ -409,16 +406,18 @@ class Controller:
)
elif root_create_data["create_type"] == "import_server":
_copy_import_dir_files(create_data["existing_server_path"])
full_jar_path = os.path.join(new_server_path, create_data["jarfile"])
server_file = create_data["jarfile"]
elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory
full_jar_path = os.path.join(new_server_path, create_data["jarfile"])
server_file = create_data["jarfile"]
raise Exception("Not yet implemented")
_create_server_properties_if_needed(create_data["server_properties_port"])
min_mem = create_data["mem_min"]
max_mem = create_data["mem_max"]
full_jar_path = os.path.join(new_server_path, server_file)
def _gibs_to_mibs(gibs: float) -> str:
return str(int(gibs * 1024))
@ -448,7 +447,9 @@ class Controller:
_create_server_properties_if_needed(0, True)
server_command = create_data["command"]
server_file = ""
server_file = (
"./bedrock_server" # HACK: This is a hack to make the server start
)
elif data["create_type"] == "custom":
# TODO: working_directory, executable_update
if root_create_data["create_type"] == "raw_exec":
@ -468,7 +469,11 @@ class Controller:
_create_server_properties_if_needed(0, True)
server_command = create_data["command"]
server_file = root_create_data["executable_update"].get("file", "")
server_file_new = root_create_data["executable_update"].get("file", "")
if server_file_new != "":
# HACK: Horrible hack to make the server start
server_file = server_file_new
stop_command = data.get("stop_command", "")
if stop_command == "":
@ -478,7 +483,7 @@ class Controller:
log_location = data.get("log_location", "")
if log_location == "":
# TODO: different default log locations for server creation types
log_location = "/logs/latest.log"
log_location = "./logs/latest.log"
if data["monitoring_type"] == "minecraft_java":
monitoring_port = data["minecraft_java_monitoring_data"]["port"]
@ -490,7 +495,7 @@ class Controller:
monitoring_type = "minecraft-bedrock"
elif data["monitoring_type"] == "none":
# TODO: this needs to be NUKED..
# There shouldn't be anything set if there are nothing to monitor
# There shouldn't be anything set if there is nothing to monitor
monitoring_port = 25565
monitoring_host = "127.0.0.1"
monitoring_type = "minecraft-java"

View File

@ -238,4 +238,4 @@ class BaseHandler(tornado.web.RequestHandler):
def finish_json(self, status: int, data: t.Dict[str, t.Any]):
self.set_status(status)
self.set_header("Content-Type", "application/json")
self.finish(orjson.dumps(data)) # pylint: disable=no-member
self.finish(orjson.dumps(data))

View File

@ -16,12 +16,9 @@ from tornado import iostream
# TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone
from croniter import croniter
from app.classes.controllers.roles_controller import RolesController
from app.classes.models.roles import HelperRoles
from app.classes.models.server_permissions import (
EnumPermissionsServer,
PermissionsServers,
)
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.management import HelpersManagement
from app.classes.shared.helpers import Helpers
@ -40,8 +37,8 @@ class PanelHandler(BaseHandler):
user_roles[user_id] = user_roles_list
return user_roles
def get_role_servers(self) -> t.Set[int]:
servers = set()
def get_role_servers(self) -> t.List[RolesController.RoleServerJsonType]:
servers = []
for server in self.controller.list_defined_servers():
argument = self.get_argument(f"server_{server['server_id']}_access", "0")
if argument == "0":
@ -57,7 +54,9 @@ class PanelHandler(BaseHandler):
permission_mask, permission, "1"
)
servers.add((server["server_id"], permission_mask))
servers.append(
{"server_id": server["server_id"], "permissions": permission_mask}
)
return servers
def get_perms_quantity(self) -> t.Tuple[str, dict]:
@ -2016,35 +2015,7 @@ class PanelHandler(BaseHandler):
servers = self.get_role_servers()
# TODO: use update_role_advanced when API v2 gets merged
base_data = self.controller.roles.get_role_with_servers(role_id)
server_ids = {server[0] for server in servers}
server_permissions_map = {server[0]: server[1] for server in servers}
added_servers = server_ids.difference(set(base_data["servers"]))
removed_servers = set(base_data["servers"]).difference(server_ids)
same_servers = server_ids.intersection(set(base_data["servers"]))
logger.debug(
f"role: {role_id} +server:{added_servers} -server{removed_servers}"
)
for server_id in added_servers:
PermissionsServers.get_or_create(
role_id, server_id, server_permissions_map[server_id]
)
for server_id in same_servers:
PermissionsServers.update_role_permission(
role_id, server_id, server_permissions_map[server_id]
)
if len(removed_servers) != 0:
PermissionsServers.delete_roles_permissions(role_id, removed_servers)
up_data = {
"role_name": role_name,
"last_update": Helpers.get_time_as_string(),
}
# TODO: do the last_update on the db side
HelperRoles.update_role(role_id, up_data)
self.controller.roles.update_role_advanced(role_id, role_name, servers)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@ -2081,10 +2052,7 @@ class PanelHandler(BaseHandler):
servers = self.get_role_servers()
role_id = self.controller.roles.add_role(role_name)
# TODO: use add_role_advanced when API v2 gets merged
for server in servers:
PermissionsServers.get_or_create(role_id, server[0], server[1])
role_id = self.controller.roles.add_role_advanced(role_name, servers)
self.controller.management.add_to_audit_log(
exec_user["user_id"],

View File

@ -25,6 +25,9 @@ from app.classes.web.routes.api.servers.server.stats import ApiServersServerStat
from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler
from app.classes.web.routes.api.users.index import ApiUsersIndexHandler
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
from app.classes.web.routes.api.users.user.permissions import (
ApiUsersUserPermissionsHandler,
)
from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler
@ -58,6 +61,16 @@ def api_handlers(handler_args):
ApiUsersUserIndexHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/permissions/?",
ApiUsersUserPermissionsHandler,
handler_args,
),
(
r"/api/v2/users/(@me)/permissions/?",
ApiUsersUserPermissionsHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/pfp/?",
ApiUsersUserPfpHandler,

View File

@ -1,6 +1,5 @@
import datetime
import logging
from app.classes.shared.console import Console
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
@ -12,8 +11,7 @@ class ApiAuthInvalidateTokensHandler(BaseApiHandler):
if not auth_data:
return
# TODO: Invalidate tokens
Console.info("invalidate_tokens")
logger.debug(f"Invalidate tokens for user {auth_data[4]['user_id']}")
self.controller.users.raw_update_user(
auth_data[4]["user_id"], {"valid_tokens_from": datetime.datetime.now()}
)

View File

@ -79,8 +79,8 @@ class ApiRolesIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)

View File

@ -105,8 +105,8 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)

View File

@ -17,24 +17,6 @@ new_server_schema = {
"monitoring_type",
"create_type",
],
"examples": [
{
"name": "My Server",
"monitoring_type": "minecraft_java",
"minecraft_java_monitoring_data": {"host": "127.0.0.1", "port": 25565},
"create_type": "minecraft_java",
"minecraft_java_create_data": {
"create_type": "download_jar",
"download_jar_create_data": {
"type": "Paper",
"version": "1.18.2",
"mem_min": 1,
"mem_max": 2,
"server_properties_port": 25565,
},
},
}
],
"properties": {
"name": {
"title": "Name",
@ -665,8 +647,8 @@ class ApiServersIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)

View File

@ -110,7 +110,7 @@ class ApiServersServerIndexHandler(BaseApiHandler):
server_obj = self.controller.servers.get_server_obj(server_id)
for key in data:
# If we don't validate the input there could be security issues
setattr(self, key, data[key])
setattr(server_obj, key, data[key])
self.controller.servers.update_server(server_obj)
self.controller.management.add_to_audit_log(

View File

@ -99,7 +99,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
email = data.get("email", "default@example.com")
enabled = data.get("enabled", True)
lang = data.get("lang", self.helper.get_setting("language"))
superuser = data.get("superuser", False)
new_superuser = data.get("superuser", False)
permissions = data.get("permissions", None)
roles = data.get("roles", None)
hints = data.get("hints", True)
@ -134,13 +134,24 @@ class ApiUsersIndexHandler(BaseApiHandler):
)
permissions_mask = "".join(permissions_mask)
if new_superuser and not superuser:
return self.finish_json(
400, {"status": "error", "error": "INVALID_SUPERUSER_CREATE"}
)
if len(roles) != 0 and not superuser:
# HACK: This should check if the user has the roles or something
return self.finish_json(
400, {"status": "error", "error": "INVALID_ROLES_CREATE"}
)
# TODO: do this in the most efficient way
user_id = self.controller.users.add_user(
username,
password,
email,
enabled,
superuser,
new_superuser,
)
self.controller.users.update_user(
user_id,

View File

@ -1,8 +1,13 @@
import json
import logging
import typing as t
from jsonschema import ValidationError, validate
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.controllers.users_controller import UsersController
from app.classes.models.crafty_permissions import (
EnumPermissionsCrafty,
PermissionsCrafty,
)
from app.classes.models.roles import HelperRoles
from app.classes.models.users import HelperUsers
from app.classes.web.base_api_handler import BaseApiHandler
@ -170,7 +175,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
},
)
if data.get("username", None) is not None:
if "username" in data:
if data["username"].lower() in ["system", ""]:
return self.finish_json(
400, {"status": "error", "error": "INVALID_USERNAME"}
@ -180,10 +185,10 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "USER_EXISTS"}
)
if data.get("superuser", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change super user status of self.
# We don't want that.
if "superuser" in data:
if str(user["user_id"]) == str(user_id) and not superuser:
# Checks if user is trying to change super user status
# of self without superuser. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_SUPERUSER_MODIFY"}
)
@ -191,10 +196,10 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
# The user is not superuser so they can't change the superuser status
data.pop("superuser")
if data.get("permissions", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change permissions of self.
# We don't want that.
if "permissions" in data:
if str(user["user_id"]) == str(user_id) and not superuser:
# Checks if user is trying to change permissions
# of self without superuser. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_PERMISSIONS_MODIFY"}
)
@ -205,10 +210,10 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_PERMISSIONS_MODIFY"}
)
if data.get("roles", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change roles of self.
# We don't want that.
if "roles" in data:
if str(user["user_id"]) == str(user_id) and not superuser:
# Checks if user is trying to change roles of
# self without superuser. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
)
@ -219,10 +224,66 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
)
# TODO: make this more efficient
# TODO: add permissions and roles because I forgot
if "password" in data and str(user["user_id"] == str(user_id)):
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
)
user_obj = HelperUsers.get_user_model(user_id)
if "roles" in data:
roles: t.Set[str] = set(data.pop("roles"))
base_roles: t.Set[str] = set(user_obj.roles)
added_roles = roles.difference(base_roles)
removed_roles = base_roles.difference(roles)
logger.debug(
f"updating user {user_id}'s roles: "
f"+role:{added_roles} -role:{removed_roles}"
)
for role_id in added_roles:
HelperUsers.get_or_create(user_id, role_id)
if len(removed_roles) != 0:
self.controller.users.users_helper.delete_user_roles(
user_id, removed_roles
)
if "permissions" in data:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions"
)
permissions_mask = "0" * len(EnumPermissionsCrafty)
limit_server_creation = 0
limit_user_creation = 0
limit_role_creation = 0
for permission in permissions:
self.controller.crafty_perms.set_permission(
permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0",
)
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
# TODO: make this more efficient
if len(data) != 0:
for key in data:
# If we don't validate the input there could be security issues
value = data[key]
if key == "password":
value = self.helper.encode_pass(value)
setattr(user_obj, key, value)
user_obj.save()
self.controller.management.add_to_audit_log(
user["user_id"],
(
@ -233,9 +294,4 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
source_ip=self.get_remote_ip(),
)
for key in data:
# If we don't validate the input there could be security issues
setattr(user_obj, key, data[key])
user_obj.save()
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,73 @@
import logging
import typing as t
from app.classes.models.crafty_permissions import (
EnumPermissionsCrafty,
PermissionsCrafty,
)
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
SERVER_CREATION: t.Final[str] = EnumPermissionsCrafty.SERVER_CREATION.name
USER_CONFIG: t.Final[str] = EnumPermissionsCrafty.USER_CONFIG.name
ROLES_CONFIG: t.Final[str] = EnumPermissionsCrafty.ROLES_CONFIG.name
class ApiUsersUserPermissionsHandler(BaseApiHandler):
def get(self, user_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
_,
user,
) = auth_data
if user_id in ["@me", user["user_id"]]:
user_id = user["user_id"]
res_data = PermissionsCrafty.get_user_crafty(user_id)
elif EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
},
)
else:
# has User_Config permission and isn't viewing self
res_data = PermissionsCrafty.get_user_crafty_optional(user_id)
if res_data is None:
return self.finish_json(
404,
{
"status": "error",
"error": "USER_NOT_FOUND",
},
)
self.finish_json(
200,
{
"status": "ok",
"data": {
"permissions": res_data.permissions,
"counters": {
SERVER_CREATION: res_data.created_server,
USER_CONFIG: res_data.created_user,
ROLES_CONFIG: res_data.created_role,
},
"limits": {
SERVER_CREATION: res_data.limit_server_creation,
USER_CONFIG: res_data.limit_user_creation,
ROLES_CONFIG: res_data.limit_role_creation,
},
},
},
)

View File

@ -17,5 +17,5 @@ requests==2.26
termcolor==1.1
tornado==6.0
tzlocal==4.0
jsonschema==4.4.0
jsonschema==4.5.1
orjson==3.6.7