acl frontend started
This commit is contained in:
parent
ec7fb0ecc3
commit
603b3676d2
@ -3,12 +3,25 @@
|
|||||||
namespace Core\API {
|
namespace Core\API {
|
||||||
|
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
|
use Core\Objects\DatabaseEntity\ApiKey;
|
||||||
|
|
||||||
abstract class ApiKeyAPI extends Request {
|
abstract class ApiKeyAPI extends Request {
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||||
parent::__construct($context, $externalCall, $params);
|
parent::__construct($context, $externalCall, $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function fetchAPIKey(int $apiKeyId): ApiKey|bool {
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$apiKey = ApiKey::find($sql, $apiKeyId);
|
||||||
|
if ($apiKey === false) {
|
||||||
|
return $this->createError("Error fetching API-Key details: " . $sql->getLastError());
|
||||||
|
} else if ($apiKey === null) {
|
||||||
|
return $this->createError("API-Key does not exit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $apiKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,22 +123,16 @@ namespace Core\API\ApiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
$apiKey = $this->fetchAPIKey($this->getParam("id"));
|
||||||
|
if ($apiKey) {
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$id = $this->getParam("id");
|
|
||||||
$apiKey = ApiKey::find($sql, $id);
|
|
||||||
if ($apiKey === false) {
|
|
||||||
return $this->createError("Error fetching API-Key details: " . $sql->getLastError());
|
|
||||||
} else if ($apiKey === null) {
|
|
||||||
return $this->createError("API-Key does not exit");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->success = $apiKey->refresh($sql, 30) !== false;
|
$this->success = $apiKey->refresh($sql, 30) !== false;
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
if ($this->success) {
|
if ($this->success) {
|
||||||
$this->result["validUntil"] = $apiKey->getValidUntil()->getTimestamp();
|
$this->result["validUntil"] = $apiKey->getValidUntil()->getTimestamp();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,17 +151,12 @@ namespace Core\API\ApiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
$apiKey = $this->fetchAPIKey($this->getParam("id"));
|
||||||
|
if ($apiKey) {
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$id = $this->getParam("id");
|
|
||||||
$apiKey = ApiKey::find($sql, $id);
|
|
||||||
if ($apiKey === false) {
|
|
||||||
return $this->createError("Error fetching API-Key details: " . $sql->getLastError());
|
|
||||||
} else if ($apiKey === null) {
|
|
||||||
return $this->createError("API-Key does not exit");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->success = $apiKey->revoke($sql);
|
$this->success = $apiKey->revoke($sql);
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
}
|
||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
10
cli.php
10
cli.php
@ -13,6 +13,8 @@ use Core\Driver\SQL\Condition\CondIn;
|
|||||||
use Core\Driver\SQL\Expression\DateSub;
|
use Core\Driver\SQL\Expression\DateSub;
|
||||||
use Core\Driver\SQL\SQL;
|
use Core\Driver\SQL\SQL;
|
||||||
use Core\Objects\ConnectionData;
|
use Core\Objects\ConnectionData;
|
||||||
|
|
||||||
|
// TODO: is this available in all installations?
|
||||||
use JetBrains\PhpStorm\NoReturn;
|
use JetBrains\PhpStorm\NoReturn;
|
||||||
|
|
||||||
function printLine(string $line = ""): void {
|
function printLine(string $line = ""): void {
|
||||||
@ -41,6 +43,7 @@ if (!$context->isCLI()) {
|
|||||||
_exit("Can only be executed via CLI");
|
_exit("Can only be executed via CLI");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dockerYaml = null;
|
||||||
$database = $context->getConfig()->getDatabase();
|
$database = $context->getConfig()->getDatabase();
|
||||||
if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
||||||
if (function_exists("yaml_parse")) {
|
if (function_exists("yaml_parse")) {
|
||||||
@ -181,6 +184,7 @@ function handleDatabase(array $argv): void {
|
|||||||
$command = array_merge(["docker", "exec", "-it", $containerName], $command);
|
$command = array_merge(["docker", "exec", "-it", $containerName], $command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var_dump($command);
|
||||||
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
|
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
|
||||||
|
|
||||||
if (is_resource($process)) {
|
if (is_resource($process)) {
|
||||||
@ -797,7 +801,7 @@ function onFrontend(array $argv): void {
|
|||||||
$argv = $_SERVER['argv'];
|
$argv = $_SERVER['argv'];
|
||||||
$registeredCommands = [
|
$registeredCommands = [
|
||||||
"help" => ["handler" => "printHelp"],
|
"help" => ["handler" => "printHelp"],
|
||||||
"db" => ["handler" => "handleDatabase", "requiresDocker" => ["shell", "import", "export"]],
|
"db" => ["handler" => "handleDatabase"],
|
||||||
"routes" => ["handler" => "onRoutes"],
|
"routes" => ["handler" => "onRoutes"],
|
||||||
"maintenance" => ["handler" => "onMaintenance"],
|
"maintenance" => ["handler" => "onMaintenance"],
|
||||||
"test" => ["handler" => "onTest"],
|
"test" => ["handler" => "onTest"],
|
||||||
@ -814,11 +818,13 @@ if (count($argv) < 2) {
|
|||||||
$command = $argv[1];
|
$command = $argv[1];
|
||||||
if (array_key_exists($command, $registeredCommands)) {
|
if (array_key_exists($command, $registeredCommands)) {
|
||||||
|
|
||||||
|
// TODO: do we need this?
|
||||||
if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
||||||
$requiresDocker = in_array($argv[2] ?? null, $registeredCommands[$command]["requiresDocker"] ?? []);
|
$requiresDocker = in_array($argv[2] ?? null, $registeredCommands[$command]["requiresDocker"] ?? []);
|
||||||
if ($requiresDocker) {
|
if ($requiresDocker) {
|
||||||
printLine("Detected docker environment in config, running docker exec...");
|
|
||||||
$containerName = $dockerYaml["services"]["php"]["container_name"];
|
$containerName = $dockerYaml["services"]["php"]["container_name"];
|
||||||
|
printLine("Detected docker environment in config, running docker exec for container: $containerName");
|
||||||
|
var_dump($argv);
|
||||||
$command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv);
|
$command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv);
|
||||||
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes);
|
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes);
|
||||||
exit(proc_close($proc));
|
exit(proc_close($proc));
|
||||||
|
@ -12,13 +12,14 @@ import './res/adminlte.min.css';
|
|||||||
|
|
||||||
// views
|
// views
|
||||||
import View404 from "./views/404";
|
import View404 from "./views/404";
|
||||||
import LogView from "./views/log-view";
|
import clsx from "clsx";
|
||||||
const Overview = lazy(() => import('./views/overview'));
|
const Overview = lazy(() => import('./views/overview'));
|
||||||
const UserListView = lazy(() => import('./views/user/user-list'));
|
const UserListView = lazy(() => import('./views/user/user-list'));
|
||||||
const UserEditView = lazy(() => import('./views/user/user-edit'));
|
const UserEditView = lazy(() => import('./views/user/user-edit'));
|
||||||
const GroupListView = lazy(() => import('./views/group-list'));
|
const GroupListView = lazy(() => import('./views/group-list'));
|
||||||
const EditGroupView = lazy(() => import('./views/group-edit'));
|
const EditGroupView = lazy(() => import('./views/group-edit'));
|
||||||
|
const LogView = lazy(() => import("./views/log-view"));
|
||||||
|
const AccessControlList = lazy(() => import("./views/access-control-list"));
|
||||||
|
|
||||||
export default function AdminDashboard(props) {
|
export default function AdminDashboard(props) {
|
||||||
|
|
||||||
@ -57,6 +58,11 @@ export default function AdminDashboard(props) {
|
|||||||
hideDialog: hideDialog
|
hideDialog: hideDialog
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// add fixed-layout to body, I don't want to rewrite my base.twig template
|
||||||
|
if (!document.body.className.includes("layout-fixed")) {
|
||||||
|
document.body.className = clsx(document.body.className, "layout-fixed");
|
||||||
|
}
|
||||||
|
|
||||||
return <BrowserRouter>
|
return <BrowserRouter>
|
||||||
<Header {...controlObj} />
|
<Header {...controlObj} />
|
||||||
<Sidebar {...controlObj} />
|
<Sidebar {...controlObj} />
|
||||||
@ -72,6 +78,7 @@ export default function AdminDashboard(props) {
|
|||||||
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
|
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
|
||||||
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
|
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
|
||||||
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
|
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
|
||||||
|
<Route path={"/admin/acl"} element={<AccessControlList {...controlObj} />}/>
|
||||||
<Route path={"*"} element={<View404 />} />
|
<Route path={"*"} element={<View404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -40,6 +40,10 @@ export default function Sidebar(props) {
|
|||||||
"name": "admin.settings",
|
"name": "admin.settings",
|
||||||
"icon": "tools"
|
"icon": "tools"
|
||||||
},
|
},
|
||||||
|
"acl": {
|
||||||
|
"name": "admin.acl",
|
||||||
|
"icon": "door-open"
|
||||||
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"name": "admin.logs",
|
"name": "admin.logs",
|
||||||
"icon": "file-medical-alt"
|
"icon": "file-medical-alt"
|
||||||
@ -71,7 +75,7 @@ export default function Sidebar(props) {
|
|||||||
</a>
|
</a>
|
||||||
</li>);
|
</li>);
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<aside className={"main-sidebar sidebar-dark-primary elevation-4"}>
|
<aside className={"main-sidebar sidebar-dark-primary elevation-4"}>
|
||||||
<Link href={"#"} className={"brand-link"} to={"/admin/dashboard"}>
|
<Link href={"#"} className={"brand-link"} to={"/admin/dashboard"}>
|
||||||
<img src={"/img/icons/logo.png"} alt={"Logo"} className={"brand-image img-circle elevation-3"} style={{opacity: ".8"}} />
|
<img src={"/img/icons/logo.png"} alt={"Logo"} className={"brand-image img-circle elevation-3"} style={{opacity: ".8"}} />
|
||||||
@ -92,7 +96,7 @@ export default function Sidebar(props) {
|
|||||||
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
|
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
|
||||||
<div className="user-panel mt-3 pb-3 mb-3 d-flex">
|
<div className="user-panel mt-3 pb-3 mb-3 d-flex">
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<span className={"d-block"}>{L("account.logged_in_as")}:
|
<span className={"d-block text-light"}>{L("account.logged_in_as")}:
|
||||||
<Link to={"/admin/user/" + api.user.id}>{api.user.name}</Link>
|
<Link to={"/admin/user/" + api.user.id}>{api.user.name}</Link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -107,5 +111,5 @@ export default function Sidebar(props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
</>
|
||||||
}
|
}
|
||||||
|
143
react/admin-panel/src/views/access-control-list.js
Normal file
143
react/admin-panel/src/views/access-control-list.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {LocaleContext} from "shared/locale";
|
||||||
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
|
import {Button, Checkbox, TextField, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core";
|
||||||
|
import {Add, Refresh} from "@material-ui/icons";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AccessControlList(props) {
|
||||||
|
|
||||||
|
// meta
|
||||||
|
const showDialog = props.showDialog;
|
||||||
|
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// data
|
||||||
|
const [acl, setACL] = useState([]);
|
||||||
|
const [groups, setGroups] = useState([]);
|
||||||
|
const [fetchACL, setFetchACL] = useState(true);
|
||||||
|
|
||||||
|
// filters
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const onFetchACL = useCallback((force = false) => {
|
||||||
|
if (force || fetchACL) {
|
||||||
|
setFetchACL(false);
|
||||||
|
props.api.fetchGroups().then(res => {
|
||||||
|
if (!res.success) {
|
||||||
|
props.showDialog(res.msg, "Error fetching groups");
|
||||||
|
navigate("/admin/dashboard");
|
||||||
|
} else {
|
||||||
|
setGroups(res.groups);
|
||||||
|
props.api.fetchPermissions().then(res => {
|
||||||
|
if (!res.success) {
|
||||||
|
props.showDialog(res.msg, "Error fetching permissions");
|
||||||
|
navigate("/admin/dashboard");
|
||||||
|
} else {
|
||||||
|
setACL(res.permissions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [fetchACL]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFetchACL();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
requestModules(props.api, ["general"], currentLocale).then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
props.showDialog("Error fetching translations: " + data.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [currentLocale]);
|
||||||
|
|
||||||
|
const PermissionList = () => {
|
||||||
|
let rows = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < acl.length; index++) {
|
||||||
|
const permission = acl[index];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
if (!permission.method.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
!permission.description.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
<TableRow key={"perm-" + index}>
|
||||||
|
<TableCell>
|
||||||
|
<b>{permission.method}</b><br />
|
||||||
|
<i>{permission.description}</i>
|
||||||
|
</TableCell>
|
||||||
|
{groups.map(group => <TableCell key={"perm-" + index + "-group-" + group.id} align={"center"}>
|
||||||
|
<Checkbox checked={!permission.groups.length || permission.groups.includes(group.id)} />
|
||||||
|
</TableCell>)}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{rows}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className={"content-header"}>
|
||||||
|
<div className={"container-fluid"}>
|
||||||
|
<div className={"row mb-2"}>
|
||||||
|
<div className={"col-sm-6"}>
|
||||||
|
<h1 className={"m-0 text-dark"}>Access Control List</h1>
|
||||||
|
</div>
|
||||||
|
<div className={"col-sm-6"}>
|
||||||
|
<ol className={"breadcrumb float-sm-right"}>
|
||||||
|
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||||
|
<li className="breadcrumb-item active">ACL</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"row"}>
|
||||||
|
<div className={"col-6"}>
|
||||||
|
<div className={"form-group"}>
|
||||||
|
<label>{L("query")}</label>
|
||||||
|
<TextField
|
||||||
|
className={"form-control"}
|
||||||
|
placeholder={L("search_query") + "…"}
|
||||||
|
value={query}
|
||||||
|
onChange={e => setQuery(e.target.value)}
|
||||||
|
variant={"outlined"}
|
||||||
|
size={"small"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"col-6 text-right"}>
|
||||||
|
<label>{L("general.controls")}</label>
|
||||||
|
<div className={"form-group"}>
|
||||||
|
<Button variant={"outlined"} color={"primary"} className={"m-1"} startIcon={<Refresh />} onClick={() => onFetchACL(true)}>
|
||||||
|
{L("general.reload")}
|
||||||
|
</Button>
|
||||||
|
<Button variant={"outlined"} className={"m-1"} startIcon={<Add />} onClick={() => navigate("/admin/acl/new")}>
|
||||||
|
{L("general.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table size={"small"}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{width: "auto"}}>{L("permission")}</TableCell>
|
||||||
|
{ groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>{group.name}</TableCell>) }
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
<PermissionList />
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
@ -12,13 +12,14 @@ import {format, toDate} from "date-fns";
|
|||||||
|
|
||||||
export default function LogView(props) {
|
export default function LogView(props) {
|
||||||
|
|
||||||
//
|
// meta
|
||||||
const LOG_LEVELS = ['debug', 'info', 'warning', 'error', 'severe'];
|
|
||||||
|
|
||||||
const api = props.api;
|
const api = props.api;
|
||||||
const showDialog = props.showDialog;
|
const showDialog = props.showDialog;
|
||||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||||
const pagination = usePagination();
|
const pagination = usePagination();
|
||||||
|
|
||||||
|
// data
|
||||||
|
const LOG_LEVELS = ['debug', 'info', 'warning', 'error', 'severe'];
|
||||||
const [logEntries, setLogEntries] = useState([]);
|
const [logEntries, setLogEntries] = useState([]);
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
@ -68,7 +69,7 @@ export default function LogView(props) {
|
|||||||
new NumericColumn(L("general.id"), "id"),
|
new NumericColumn(L("general.id"), "id"),
|
||||||
new StringColumn(L("module"), "module"),
|
new StringColumn(L("module"), "module"),
|
||||||
new StringColumn(L("severity"), "severity"),
|
new StringColumn(L("severity"), "severity"),
|
||||||
new DateTimeColumn(L("timestamp"), "timestamp"),
|
new DateTimeColumn(L("timestamp"), "timestamp", { precise: true }),
|
||||||
messageColumn,
|
messageColumn,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user