From 50ae32595d712017b3ffa001675285190acba6dc Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 27 Mar 2024 20:50:57 +0100 Subject: [PATCH] permission update, routes, etc. --- Core/API/PermissionAPI.class.php | 21 +++++----- Core/API/RoutesAPI.class.php | 42 +++++++++++++++---- Core/API/UserAPI.class.php | 33 +++++++-------- Core/Documents/Security.class.php | 8 +++- Core/Localization/de_DE/admin.php | 3 ++ Core/Localization/en_US/admin.php | 5 ++- Core/Objects/DatabaseEntity/Route.class.php | 6 ++- Core/Objects/Router/DocumentRoute.class.php | 2 +- react/admin-panel/src/AdminDashboard.jsx | 4 +- react/admin-panel/src/elements/sidebar.js | 6 ++- .../src/views/{ => group}/group-edit.js | 0 .../src/views/{ => group}/group-list.js | 42 +++++++++++++------ react/shared/elements/data-table.js | 4 +- react/shared/elements/dialog.jsx | 3 +- react/shared/hooks/pagination.js | 21 ++++++---- 15 files changed, 131 insertions(+), 69 deletions(-) rename react/admin-panel/src/views/{ => group}/group-edit.js (100%) rename react/admin-panel/src/views/{ => group}/group-list.js (69%) diff --git a/Core/API/PermissionAPI.class.php b/Core/API/PermissionAPI.class.php index 21d2921..86297d4 100644 --- a/Core/API/PermissionAPI.class.php +++ b/Core/API/PermissionAPI.class.php @@ -34,6 +34,7 @@ namespace Core\API\Permission { use Core\API\Parameter\StringType; use Core\API\PermissionAPI; use Core\Driver\SQL\Column\Column; + use Core\Driver\SQL\Condition\CondIn; use Core\Driver\SQL\Condition\CondLike; use Core\Driver\SQL\Query\Insert; use Core\Driver\SQL\Strategy\UpdateStrategy; @@ -168,25 +169,25 @@ namespace Core\API\Permission { return $this->createError("This method cannot be updated."); } - $groups = $this->getParam("groups"); - if (!empty($groups)) { - sort($groups); - $availableGroups = Group::findAll($sql); - foreach ($groups as $groupId) { + $groupIds = array_unique($this->getParam("groups")); + if (!empty($groupIds)) { + sort($groupIds); + $availableGroups = Group::findAll($sql, new CondIn(new Column("id"), $groupIds)); + foreach ($groupIds as $groupId) { if (!isset($availableGroups[$groupId])) { - return $this->createError("Unknown group id: $groupId"); + return $this->createError("Group with id=$groupId does not exist."); } } } if ($description === null) { $updateQuery = $sql->insert("ApiPermission", ["method", "groups", "isCore"]) - ->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groups])) - ->addRow($method, $groups, false); + ->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groupIds])) + ->addRow($method, $groupIds, false); } else { $updateQuery = $sql->insert("ApiPermission", ["method", "groups", "isCore", "description"]) - ->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groups, "description" => $description])) - ->addRow($method, $groups, false, $description); + ->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groupIds, "description" => $description])) + ->addRow($method, $groupIds, false, $description); } $this->success = $updateQuery->execute() !== false; diff --git a/Core/API/RoutesAPI.class.php b/Core/API/RoutesAPI.class.php index 6a00413..2b5a0f3 100644 --- a/Core/API/RoutesAPI.class.php +++ b/Core/API/RoutesAPI.class.php @@ -67,19 +67,11 @@ namespace Core\API\Routes { use Core\API\Parameter\Parameter; use Core\API\Parameter\StringType; use Core\API\RoutesAPI; - use Core\Driver\SQL\Condition\Compare; - use Core\Driver\SQL\Condition\CondBool; use Core\Driver\SQL\Query\Insert; - use Core\Driver\SQL\Query\StartTransaction; use Core\Objects\Context; use Core\Objects\DatabaseEntity\Group; use Core\Objects\DatabaseEntity\Route; - use Core\Objects\Router\DocumentRoute; - use Core\Objects\Router\RedirectPermanentlyRoute; - use Core\Objects\Router\RedirectRoute; - use Core\Objects\Router\RedirectTemporaryRoute; use Core\Objects\Router\Router; - use Core\Objects\Router\StaticFileRoute; class Fetch extends RoutesAPI { @@ -419,5 +411,39 @@ namespace Core\API\Routes { $insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to regenerate the routing cache", true); } } + + class Check extends RoutesAPI { + public function __construct(Context $context, bool $externalCall) { + parent::__construct($context, $externalCall, [ + "id" => new Parameter("id", Parameter::TYPE_INT), + "path" => new StringType("path") + ]); + } + + protected function _execute(): bool { + $sql = $this->context->getSQL(); + $routeId = $this->getParam("id"); + $route = Route::find($sql, $routeId); + if ($route === false) { + $this->lastError = $sql->getLastError(); + return false; + } else if ($route === null) { + return $this->createError("Route not found"); + } + + $this->success = true; + $path = $this->getParam("path"); + $this->result["match"] = $route->match($path); + return $this->success; + } + + public static function getDefaultACL(Insert $insert): void { + $insert->addRow("routes/check", + [Group::ADMIN, Group::MODERATOR], + "Users with this permission can see, if a route is matched with the given path for debugging purposes", + true + ); + } + } } diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php index 0a2aee3..a08883f 100644 --- a/Core/API/UserAPI.class.php +++ b/Core/API/UserAPI.class.php @@ -183,14 +183,14 @@ namespace Core\API\User { $groups = []; $sql = $this->context->getSQL(); - // TODO: Currently low-privileged users can request any groups here, so a simple privilege escalation is possible. \ - // what do? limit access to user/create to admins only? $requestedGroups = array_unique($this->getParam("groups")); if (!empty($requestedGroups)) { - $groups = Group::findAll($sql, new CondIn(new Column("id"), $requestedGroups)); + $availableGroups = Group::findAll($sql, new CondIn(new Column("id"), $requestedGroups)); foreach ($requestedGroups as $groupId) { - if (!isset($groups[$groupId])) { + if (!isset($availableGroups[$groupId])) { return $this->createError("Group with id=$groupId does not exist."); + } else if ($groupId === Group::ADMIN && !$this->context->getUser()->hasGroup(Group::ADMIN)) { + return $this->createError("You cannot create users with administrator groups."); } } } @@ -632,7 +632,7 @@ namespace Core\API\User { public function __construct(Context $context, bool $externalCall = false) { $parameters = array( "username" => new StringType("username", 32), - 'email' => new Parameter('email', Parameter::TYPE_EMAIL), + "email" => new Parameter("email", Parameter::TYPE_EMAIL), "password" => new StringType("password"), "confirmPassword" => new StringType("confirmPassword"), ); @@ -746,7 +746,7 @@ namespace Core\API\User { 'fullName' => new StringType('fullName', 64, true, NULL), 'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL), 'password' => new StringType('password', -1, true, NULL), - 'groups' => new Parameter('groups', Parameter::TYPE_ARRAY, true, NULL), + 'groups' => new ArrayType('groups', Parameter::TYPE_INT, true, true, NULL), 'confirmed' => new Parameter('confirmed', Parameter::TYPE_BOOLEAN, true, NULL) )); @@ -777,19 +777,18 @@ namespace Core\API\User { $groupIds = array(); if (!is_null($groups)) { - $param = new Parameter('groupId', Parameter::TYPE_INT); - - foreach ($groups as $groupId) { - if (!$param->parseParam($groupId)) { - $value = print_r($groupId, true); - return $this->createError("Invalid Type for groupId in parameter groups: '$value' (Required: " . $param->getTypeName() . ")"); - } - - $groupIds[] = $param->value; - } - + $groupIds = array_unique($groups); if ($id === $currentUser->getId() && !in_array(Group::ADMIN, $groupIds)) { return $this->createError("Cannot remove Administrator group from own user."); + } else if (in_array(Group::ADMIN, $groupIds) && !$currentUser->hasGroup(Group::ADMIN)) { + return $this->createError("You cannot add the administrator group to other users."); + } + + $availableGroups = Group::findAll($sql, new CondIn(new Column("id"), $groupIds)); + foreach ($groupIds as $groupId) { + if (!isset($availableGroups[$groupId])) { + return $this->createError("Group with id=$groupId does not exist."); + } } } diff --git a/Core/Documents/Security.class.php b/Core/Documents/Security.class.php index 736a89d..1cc967a 100644 --- a/Core/Documents/Security.class.php +++ b/Core/Documents/Security.class.php @@ -8,6 +8,7 @@ use Core\Elements\Document; use Core\Objects\DatabaseEntity\GpgKey; use Core\Objects\DatabaseEntity\Language; use Core\Objects\Router\Router; +use DateTimeInterface; // Source: https://www.rfc-editor.org/rfc/rfc9116 class Security extends Document { @@ -42,12 +43,12 @@ class Security extends Document { $lines = [ "# This project is based on the open-source framework hosted on https://github.com/rhergenreder/web-base", - "# Any non-site specific issues can be reported via the github security reporting feature:", + "# Any non site-specific issues can be reported via the github security reporting feature:", "# https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability", "", "Canonical: $baseUrl/.well-known/security.txt", "Preferred-Languages: $languageCodes", - "Expires: " . $expires->format(\DateTime::ATOM), + "Expires: " . $expires->format(DateTimeInterface::ATOM), "", ]; @@ -85,6 +86,9 @@ class Security extends Document { return "Error exporting public key: " . $res["msg"]; } } + } else { + http_response_code(412); + return "No gpg key configured yet."; } } diff --git a/Core/Localization/de_DE/admin.php b/Core/Localization/de_DE/admin.php index 3fb5441..d1af028 100644 --- a/Core/Localization/de_DE/admin.php +++ b/Core/Localization/de_DE/admin.php @@ -5,8 +5,11 @@ return [ "dashboard" => "Dashboard", "visitor_statistics" => "Besucherstatistiken", "user_groups" => "Benutzer & Gruppen", + "users" => "Benutzer", + "groups" => "Gruppen", "page_routes" => "Seiten & Routen", "settings" => "Einstellungen", + "acl" => "Zugriffsberechtigung", "logs" => "Logs", "help" => "Hilfe", ]; \ No newline at end of file diff --git a/Core/Localization/en_US/admin.php b/Core/Localization/en_US/admin.php index 3e822b6..68a12e9 100644 --- a/Core/Localization/en_US/admin.php +++ b/Core/Localization/en_US/admin.php @@ -4,9 +4,12 @@ return [ "title" => "Administration", "dashboard" => "Dashboard", "visitor_statistics" => "Visitor Statistics", - "user_groups" => "User & Groups", + "user_groups" => "Users & Groups", + "users" => "Users", + "groups" => "Groups", "page_routes" => "Pages & Routes", "settings" => "Settings", + "acl" => "Access Control", "logs" => "Logs", "help" => "Help", ]; \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Route.class.php b/Core/Objects/DatabaseEntity/Route.class.php index 898b73d..45cb640 100644 --- a/Core/Objects/DatabaseEntity/Route.class.php +++ b/Core/Objects/DatabaseEntity/Route.class.php @@ -56,6 +56,10 @@ abstract class Route extends DatabaseEntity { $this->active = true; } + public function isActive(): bool { + return $this->active; + } + private static function parseParamType(?string $type): ?int { if ($type === null || trim($type) === "") { return null; @@ -115,7 +119,7 @@ abstract class Route extends DatabaseEntity { } } - public function match(string $url) { + public function match(string $url): bool|array { # /test/{abc}/{param:?}/{xyz:int}/{aaa:int?} $patternParts = self::getParts(Router::cleanURL($this->pattern, false)); diff --git a/Core/Objects/Router/DocumentRoute.class.php b/Core/Objects/Router/DocumentRoute.class.php index 906456a..bb2b8c1 100644 --- a/Core/Objects/Router/DocumentRoute.class.php +++ b/Core/Objects/Router/DocumentRoute.class.php @@ -61,7 +61,7 @@ class DocumentRoute extends Route { return true; } - public function match(string $url) { + public function match(string $url): bool|array { $match = parent::match($url); if ($match === false || !$this->loadClass()) { return false; diff --git a/react/admin-panel/src/AdminDashboard.jsx b/react/admin-panel/src/AdminDashboard.jsx index d89da14..d846fab 100644 --- a/react/admin-panel/src/AdminDashboard.jsx +++ b/react/admin-panel/src/AdminDashboard.jsx @@ -16,8 +16,8 @@ import clsx from "clsx"; const Overview = lazy(() => import('./views/overview')); const UserListView = lazy(() => import('./views/user/user-list')); const UserEditView = lazy(() => import('./views/user/user-edit')); -const GroupListView = lazy(() => import('./views/group-list')); -const EditGroupView = lazy(() => import('./views/group-edit')); +const GroupListView = lazy(() => import('./views/group/group-list')); +const EditGroupView = lazy(() => import('./views/group/group-edit')); const LogView = lazy(() => import("./views/log-view")); const AccessControlList = lazy(() => import("./views/access-control-list")); diff --git a/react/admin-panel/src/elements/sidebar.js b/react/admin-panel/src/elements/sidebar.js index 6fe17a7..658e3fb 100644 --- a/react/admin-panel/src/elements/sidebar.js +++ b/react/admin-panel/src/elements/sidebar.js @@ -29,9 +29,13 @@ export default function Sidebar(props) { "icon": "chart-bar", }, "users": { - "name": "admin.user_groups", + "name": "admin.users", "icon": "users" }, + "groups": { + "name": "admin.groups", + "icon": "users-cog" + }, "pages": { "name": "admin.page_routes", "icon": "copy", diff --git a/react/admin-panel/src/views/group-edit.js b/react/admin-panel/src/views/group/group-edit.js similarity index 100% rename from react/admin-panel/src/views/group-edit.js rename to react/admin-panel/src/views/group/group-edit.js diff --git a/react/admin-panel/src/views/group-list.js b/react/admin-panel/src/views/group/group-list.js similarity index 69% rename from react/admin-panel/src/views/group-list.js rename to react/admin-panel/src/views/group/group-list.js index a1ab3fa..9c3b3d2 100644 --- a/react/admin-panel/src/views/group-list.js +++ b/react/admin-panel/src/views/group/group-list.js @@ -5,30 +5,40 @@ import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/element import {Button, IconButton} from "@material-ui/core"; import EditIcon from '@mui/icons-material/Edit'; import AddIcon from '@mui/icons-material/Add'; +import usePagination from "shared/hooks/pagination"; export default function GroupListView(props) { + // meta const {translate: L, requestModules, currentLocale} = useContext(LocaleContext); const navigate = useNavigate(); + const pagination = usePagination(); + const api = props.api; + + // data + const [groups, setGroups] = useState([]); useEffect(() => { requestModules(props.api, ["general", "account"], currentLocale).then(data => { if (!data.success) { - alert(data.msg); + props.showDialog(data.msg, "Error fetching localization"); } }); }, [currentLocale]); const onFetchGroups = useCallback(async (page, count, orderBy, sortOrder) => { - let res = await props.api.fetchGroups(page, count, orderBy, sortOrder); - if (res.success) { - return Promise.resolve([res.groups, res.pagination]); - } else { - props.showAlert("Error fetching groups", res.msg); - return null; - } - }, []); + + api.fetchGroups(page, count, orderBy, sortOrder).then((res) => { + if (res.success) { + setGroups(res.groups); + pagination.update(res.pagination); + } else { + props.showDialog(res.msg, "Error fetching groups"); + return null; + } + }); + }, [api, pagination]); const actionColumn = (() => { let column = new DataColumn(L("general.actions"), null, false); @@ -69,10 +79,16 @@ export default function GroupListView(props) { {L("general.create_new")} - + diff --git a/react/shared/elements/data-table.js b/react/shared/elements/data-table.js index f17be42..537f254 100644 --- a/react/shared/elements/data-table.js +++ b/react/shared/elements/data-table.js @@ -123,8 +123,8 @@ export function DataTable(props) { {title ?

{fetchData ? - onFetchData(true)}> - + onFetchData(true)} title={L("general.reload")}> +   : <> } diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx index c415f0d..dc808f8 100644 --- a/react/shared/elements/dialog.jsx +++ b/react/shared/elements/dialog.jsx @@ -50,6 +50,7 @@ export default function Dialog(props) { switch (input.type) { case 'label': + delete inputProps.value; inputElements.push({input.value}); break; case 'text': @@ -57,11 +58,9 @@ export default function Dialog(props) { inputElements.push( setInputData({ ...inputData, [input.name]: e.target.value })} />) break; diff --git a/react/shared/hooks/pagination.js b/react/shared/hooks/pagination.js index 7879c44..a1093d8 100644 --- a/react/shared/hooks/pagination.js +++ b/react/shared/hooks/pagination.js @@ -1,6 +1,7 @@ import React, {useState} from "react"; import {Box, MenuItem, Select, Pagination as MuiPagination} from "@mui/material"; import {sprintf} from "sprintf-js"; +import {FormControl} from "@material-ui/core"; class Pagination { @@ -57,15 +58,17 @@ class Pagination { options = options || [10, 25, 50, 100]; return - + + + this.setPage(page)}