Add more advanced role APIs

This commit is contained in:
luukas 2022-05-10 02:08:49 +03:00
parent bf59e2de6c
commit 930c6936d9
13 changed files with 362 additions and 27 deletions

View File

@ -1,7 +1,8 @@
import logging
import typing as t
from app.classes.models.roles import HelperRoles
from app.classes.models.server_permissions import PermissionsServers
from app.classes.models.server_permissions import PermissionsServers, RoleServers
from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__)
@ -66,6 +67,90 @@ class RolesController:
def add_role(role_name):
return HelperRoles.add_role(role_name)
class RoleServerJsonType(t.TypedDict):
server_id: t.Union[str, int]
permissions: str
@staticmethod
def get_server_ids_and_perms_from_role(
role_id: t.Union[str, int]
) -> t.List[RoleServerJsonType]:
# FIXME: somehow retrieve only the server ids, not the whole servers
return [
{
"server_id": role_servers.server_id.server_id,
"permissions": role_servers.permissions,
}
for role_servers in (
RoleServers.select(
RoleServers.server_id, RoleServers.permissions
).where(RoleServers.role_id == role_id)
)
]
@staticmethod
def add_role_advanced(
name: str,
servers: t.Iterable[RoleServerJsonType],
) -> int:
"""Add a role with a name and a list of servers
Args:
name (str): The new role's name
servers (t.List[RoleServerJsonType]): The new role's servers
Returns:
int: The new role's ID
"""
role_id: t.Final[int] = HelperRoles.add_role(name)
for server in servers:
PermissionsServers.get_or_create(
role_id, server["server_id"], server["permissions"]
)
return role_id
@staticmethod
def update_role_advanced(
role_id: t.Union[str, int],
role_name: t.Optional[str],
servers: t.Optional[t.Iterable[RoleServerJsonType]],
) -> None:
"""Update a role with a name and a list of servers
Args:
role_id (t.Union[str, int]): The ID of the role to be modified
role_name (t.Optional[str]): An optional new name for the role
servers (t.Optional[t.Iterable[RoleServerJsonType]]): An optional list of servers for the role
""" # pylint: disable=line-too-long
logger.debug(f"updating role {role_id} with advanced options")
if servers is not None:
base_data = RolesController.get_role_with_servers(role_id)
server_ids = {server["server_id"] for server in servers}
server_permissions_map = {
server["server_id"]: server["permissions"] for server in servers
}
added_servers = server_ids.difference(set(base_data["servers"]))
removed_servers = set(base_data["servers"]).difference(server_ids)
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]
)
if len(removed_servers) != 0:
PermissionsServers.delete_roles_permissions(role_id, removed_servers)
if role_name is not None:
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)
def remove_role(self, role_id):
role_data = RolesController.get_role_with_servers(role_id)
PermissionsServers.delete_roles_permissions(role_id, role_data["servers"])

View File

@ -76,7 +76,10 @@ class UsersController:
},
"roles": {
"type": "array",
"items": {"type": "string"},
"items": {
"type": "string",
"minLength": 1,
},
},
"hints": {"type": "boolean"},
}

View File

@ -86,12 +86,10 @@ class HelperRoles:
return Roles.update(up_data).where(Roles.role_id == role_id).execute()
def remove_role(self, role_id):
with self.database.atomic():
role = Roles.get(Roles.role_id == role_id)
return role.delete_instance()
return Roles.delete().where(Roles.role_id == role_id).execute()
@staticmethod
def role_id_exists(role_id):
def role_id_exists(role_id) -> bool:
if not HelperRoles.get_role(role_id):
return False
return True

View File

@ -179,9 +179,9 @@ class PermissionsServers:
RoleServers.save(role_server)
@staticmethod
def delete_roles_permissions(role_id, removed_servers=None):
if removed_servers is None:
removed_servers = {}
def delete_roles_permissions(
role_id: t.Union[str, int], removed_servers: t.Sequence[t.Union[str, int]]
):
return (
RoleServers.delete()
.where(RoleServers.role_id == role_id)

View File

@ -190,6 +190,10 @@ class HelperServers:
query = Servers.select()
return DatabaseShortcuts.return_rows(query)
@staticmethod
def get_all_server_ids() -> t.List[int]:
return [server.server_id for server in Servers.select(Servers.server_id)]
@staticmethod
def get_all_servers_stats():
servers = HelperServers.get_all_defined_servers()

View File

@ -5,7 +5,7 @@ import shutil
import time
import logging
import tempfile
from typing import Union
import typing as t
from peewee import DoesNotExist
# TZLocal is set as a hidden import on win pipeline
@ -276,7 +276,7 @@ class Controller:
except:
return {"percent": 0, "total_files": 0}
def get_server_obj(self, server_id: Union[str, int]) -> Union[bool, Server]:
def get_server_obj(self, server_id: t.Union[str, int]) -> t.Union[bool, Server]:
for server in self.servers_list:
if str(server["server_id"]) == str(server_id):
return server["server_obj"]
@ -297,6 +297,10 @@ class Controller:
servers = HelperServers.get_all_defined_servers()
return servers
@staticmethod
def get_all_server_ids() -> t.List[int]:
return HelperServers.get_all_server_ids()
def list_running_servers(self):
running_servers = []

View File

@ -1,7 +1,23 @@
from typing import Awaitable, Callable, Optional
from app.classes.web.base_handler import BaseHandler
class BaseApiHandler(BaseHandler):
# {{{ Disable XSRF protection on API routes
def check_xsrf_cookie(self) -> None:
# Disable XSRF protection on API routes
pass
# }}}
# {{{ 405 Method Not Allowed as JSON
def _unimplemented_method(self, *_args: str, **_kwargs: str) -> None:
self.finish_json(405, {"status": "error", "error": "METHOD_NOT_ALLOWED"})
head = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
get = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
post = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
delete = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
patch = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
put = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
options = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
# }}}

View File

@ -2,7 +2,7 @@
import time
import datetime
import os
from typing import Dict, Any, Tuple
import typing as t
import json
import logging
import threading
@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
class PanelHandler(BaseHandler):
def get_user_roles(self) -> Dict[str, list]:
def get_user_roles(self) -> t.Dict[str, list]:
user_roles = {}
for user_id in self.controller.users.get_all_user_ids():
user_roles_list = self.controller.users.get_user_roles_names(user_id)
@ -36,7 +36,7 @@ class PanelHandler(BaseHandler):
user_roles[user_id] = user_roles_list
return user_roles
def get_role_servers(self) -> set:
def get_role_servers(self) -> t.Set[int]:
servers = set()
for server in self.controller.list_defined_servers():
argument = int(
@ -50,7 +50,7 @@ class PanelHandler(BaseHandler):
servers.add(server["server_id"])
return servers
def get_perms_quantity(self) -> Tuple[str, dict]:
def get_perms_quantity(self) -> t.Tuple[str, dict]:
permissions_mask: str = "000"
server_quantity: dict = {}
for (
@ -258,7 +258,7 @@ class PanelHandler(BaseHandler):
user_order.remove(server_id)
defined_servers = page_servers
page_data: Dict[str, Any] = {
page_data: t.Dict[str, t.Any] = {
# todo: make this actually pull and compare version data
"update_available": False,
"serverTZ": get_localzone(),

View File

@ -1,6 +1,38 @@
import typing as t
from jsonschema import ValidationError, validate
import orjson
from playhouse.shortcuts import model_to_dict
from app.classes.web.base_api_handler import BaseApiHandler
create_role_schema = {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
},
"servers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"server_id": {
"type": "integer",
"minimum": 1,
},
"permissions": {
"type": "string",
"pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer
},
},
"required": ["server_id", "permissions"],
},
},
},
"required": ["name"],
"additionalProperties": False,
}
class ApiRolesIndexHandler(BaseApiHandler):
def get(self):
@ -21,7 +53,6 @@ class ApiRolesIndexHandler(BaseApiHandler):
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
# TODO: permissions
self.finish_json(
200,
{
@ -31,3 +62,73 @@ class ApiRolesIndexHandler(BaseApiHandler):
else [model_to_dict(r) for r in self.controller.roles.get_all_roles()],
},
)
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
user,
) = auth_data
if not superuser:
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
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, create_role_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
role_name = data["name"]
# Get the servers
servers_dict = {server["server_id"]: server for server in data["servers"]}
server_ids = (
{
s
for s in (
{server["server_id"] for server in data["servers"]}
& set(self.controller.get_all_server_ids())
) # Only allow existing servers
}
if "servers" in data
else set()
)
servers: t.List[dict] = [servers_dict[server_id] for server_id in server_ids]
if self.controller.roles.get_roleid_by_name(role_name) is not None:
return self.finish_json(
400, {"status": "error", "error": "ROLE_NAME_ALREADY_EXISTS"}
)
role_id = self.controller.roles.add_role_advanced(role_name, servers)
self.controller.management.add_to_audit_log(
user["user_id"],
f"created role {role_name} (RID:{role_id})",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok", "data": {"role_id": role_id}},
)

View File

@ -1,5 +1,39 @@
from jsonschema import ValidationError, validate
import orjson
from app.classes.web.base_api_handler import BaseApiHandler
modify_role_schema = {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
},
"servers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"server_id": {
"type": "string",
"minLength": 1,
},
"permissions": {
"type": "string",
"pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer
},
},
"required": ["server_id", "permissions"],
},
},
},
"anyOf": [
{"required": ["name"]},
{"required": ["servers"]},
],
"additionalProperties": False,
}
class ApiRolesRoleIndexHandler(BaseApiHandler):
def get(self, role_id: str):
@ -17,7 +51,71 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
# TODO: permissions
self.finish_json(
200,
{"status": "ok", "data": self.controller.roles.get_role(role_id)},
)
def delete(self, role_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.roles.remove_role(role_id)
self.finish_json(
200,
{"status": "ok", "data": role_id},
)
def patch(self, role_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
if not superuser:
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
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, modify_role_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
self.controller.roles.update_role_advanced(
role_id, data.get("role_name", None), data.get("servers", None)
)
self.finish_json(
200,
{"status": "ok", "data": self.controller.roles.get_role(role_id)},

View File

@ -15,6 +15,9 @@ class ApiRolesRoleServersHandler(BaseApiHandler):
_,
) = auth_data
# GET /api/v2/roles/role/servers?ids=true
get_only_ids = self.get_query_argument("ids", None) == "true"
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -22,6 +25,8 @@ class ApiRolesRoleServersHandler(BaseApiHandler):
200,
{
"status": "ok",
"data": PermissionsServers.get_server_ids_from_role(role_id),
"data": PermissionsServers.get_server_ids_from_role(role_id)
if get_only_ids
else self.controller.roles.get_server_ids_and_perms_from_role(role_id),
},
)

View File

@ -40,6 +40,7 @@ new_server_schema = {
"title": "Name",
"type": "string",
"examples": ["My Server"],
"minLength": 2,
},
"stop_command": {
"title": "Stop command",
@ -91,6 +92,7 @@ new_server_schema = {
"type": "string",
"default": "127.0.0.1",
"examples": ["127.0.0.1"],
"minLength": 1,
},
"port": {
"title": "Port",
@ -111,6 +113,7 @@ new_server_schema = {
"type": "string",
"default": "127.0.0.1",
"examples": ["127.0.0.1"],
"minLength": 1,
},
"port": {
"title": "Port",
@ -155,11 +158,13 @@ new_server_schema = {
"title": "Server JAR Type",
"type": "string",
"examples": ["Paper"],
"minLength": 1,
},
"version": {
"title": "Server JAR Version",
"type": "string",
"examples": ["1.18.2"],
"minLength": 1,
},
"mem_min": {
"title": "Minimum JVM memory (in GiBs)",
@ -206,12 +211,14 @@ new_server_schema = {
"description": "Absolute path to the old server",
"type": "string",
"examples": ["/var/opt/server"],
"minLength": 1,
},
"jarfile": {
"title": "JAR file",
"description": "The JAR file relative to the previous path",
"type": "string",
"examples": ["paper.jar", "jars/vanilla-1.12.jar"],
"minLength": 1,
},
"mem_min": {
"title": "Minimum JVM memory (in GiBs)",
@ -259,18 +266,21 @@ new_server_schema = {
"description": "Absolute path to the ZIP archive",
"type": "string",
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
"type": "string",
"examples": ["/", "/paper-server/", "server-1"],
"minLength": 1,
},
"jarfile": {
"title": "JAR file",
"description": "The JAR relative to the configured root",
"type": "string",
"examples": ["paper.jar", "jars/vanilla-1.12.jar"],
"minLength": 1,
},
"mem_min": {
"title": "Minimum JVM memory (in GiBs)",
@ -356,12 +366,14 @@ new_server_schema = {
"description": "Absolute path to the old server",
"type": "string",
"examples": ["/var/opt/server"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["LD_LIBRARY_PATH=. ./bedrock_server"],
"minLength": 1,
},
},
},
@ -375,18 +387,21 @@ new_server_schema = {
"description": "Absolute path to the ZIP archive",
"type": "string",
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
"type": "string",
"examples": ["/", "/paper-server/", "server-1"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["LD_LIBRARY_PATH=. ./bedrock_server"],
"minLength": 1,
},
},
},
@ -474,6 +489,7 @@ new_server_schema = {
"type": "string",
"default": "echo foo bar baz",
"examples": ["caddy start"],
"minLength": 1,
}
},
},
@ -487,12 +503,14 @@ new_server_schema = {
"description": "Absolute path to the old server",
"type": "string",
"examples": ["/var/opt/server"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["caddy start"],
"minLength": 1,
},
},
},
@ -506,18 +524,21 @@ new_server_schema = {
"description": "Absolute path to the ZIP archive",
"type": "string",
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
"type": "string",
"examples": ["/", "/paper-server/", "server-1"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["caddy start"],
"minLength": 1,
},
},
},

View File

@ -11,21 +11,21 @@ logger = logging.getLogger(__name__)
server_patch_schema = {
"type": "object",
"properties": {
"server_name": {"type": "string"},
"path": {"type": "string"},
"server_name": {"type": "string", "minLength": 1},
"path": {"type": "string", "minLength": 1},
"backup_path": {"type": "string"},
"executable": {"type": "string"},
"log_path": {"type": "string"},
"execution_command": {"type": "string"},
"log_path": {"type": "string", "minLength": 1},
"execution_command": {"type": "string", "minLength": 1},
"auto_start": {"type": "boolean"},
"auto_start_delay": {"type": "integer"},
"crash_detection": {"type": "boolean"},
"stop_command": {"type": "string"},
"executable_update_url": {"type": "string"},
"server_ip": {"type": "string"},
"executable_update_url": {"type": "string", "minLength": 1},
"server_ip": {"type": "string", "minLength": 1},
"server_port": {"type": "integer"},
"logs_delete_after": {"type": "integer"},
"type": {"type": "string"},
"type": {"type": "string", "minLength": 1},
},
"anyOf": [
# Require at least one property