frontend & backend update

This commit is contained in:
2023-01-16 21:47:23 +01:00
parent 1d6ff17994
commit 4cec531a25
51 changed files with 1010 additions and 571 deletions

View File

@@ -18,6 +18,7 @@ namespace Core\API\ApiKey {
use Core\API\Parameter\Parameter;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondAnd;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\ApiKey;
@@ -46,6 +47,10 @@ namespace Core\API\ApiKey {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [], "Allows users to create new API-Keys");
}
}
class Fetch extends ApiKeyAPI {
@@ -82,6 +87,10 @@ namespace Core\API\ApiKey {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [], "Allows users to fetch new API-Key");
}
}
class Refresh extends ApiKeyAPI {
@@ -112,6 +121,10 @@ namespace Core\API\ApiKey {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [], "Allows users to refresh API-Key");
}
}
class Revoke extends ApiKeyAPI {
@@ -138,5 +151,9 @@ namespace Core\API\ApiKey {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [], "Allows users to revoke API-Key");
}
}
}

View File

@@ -12,8 +12,10 @@ namespace Core\API\Database {
use Core\API\DatabaseAPI;
use Core\API\Parameter\StringType;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
use Core\Objects\DatabaseEntity\Group;
class Status extends DatabaseAPI {
@@ -29,6 +31,10 @@ namespace Core\API\Database {
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to view the database status");
}
}
class Migrate extends DatabaseAPI {
@@ -98,5 +104,9 @@ namespace Core\API\Database {
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to migrate the database structure");
}
}
}

View File

@@ -36,8 +36,8 @@ namespace Core\API\Groups {
use Core\Driver\SQL\Expression\Alias;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\Join\InnerJoin;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Controller\NMRelation;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\User;
@@ -83,6 +83,10 @@ namespace Core\API\Groups {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to fetch available groups");
}
}
class Get extends GroupsAPI {
@@ -106,6 +110,10 @@ namespace Core\API\Groups {
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to get details about a group");
}
}
class GetMembers extends GroupsAPI {
@@ -142,6 +150,9 @@ namespace Core\API\Groups {
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to fetch members of a group");
}
}
class Create extends GroupsAPI {
@@ -182,6 +193,10 @@ namespace Core\API\Groups {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to create a new group");
}
}
class Delete extends GroupsAPI {
@@ -209,6 +224,10 @@ namespace Core\API\Groups {
return $this->success;
}
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to delete a group");
}
}
class AddMember extends GroupsAPI {
@@ -247,6 +266,10 @@ namespace Core\API\Groups {
return true;
}
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to add members to a group");
}
}
class RemoveMember extends GroupsAPI {
@@ -285,6 +308,9 @@ namespace Core\API\Groups {
return true;
}
}
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to remove members from a group");
}
}
}

View File

@@ -21,7 +21,9 @@ namespace Core\API\Logs {
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\SystemLog;
class Get extends LogsAPI {
@@ -118,6 +120,10 @@ namespace Core\API\Logs {
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch system logs");
}
}
}

View File

@@ -46,6 +46,8 @@ namespace Core\API\Mail {
use Core\API\MailAPI;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\MailQueueItem;
use DateTimeInterface;
use Core\Driver\SQL\Condition\Compare;
@@ -78,6 +80,10 @@ namespace Core\API\Mail {
$this->lastError = $req->getLastError();
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to send a test email to verify configuration");
}
}
class Send extends MailAPI {

View File

@@ -31,6 +31,7 @@ namespace Core\API\Permission {
use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\SQL\Query\Insert;
use Core\Driver\SQL\Strategy\UpdateStrategy;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Group;
@@ -73,6 +74,16 @@ namespace Core\API\Permission {
http_response_code(401);
return $this->createError("Permission denied.");
}
// user would have required groups, check for 2fa-state
if ($currentUser) {
$tfaToken = $currentUser->getTwoFactorToken();
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
$this->lastError = '2FA-Authorization is required';
http_response_code(401);
return false;
}
}
}
return $this->success;
@@ -127,6 +138,10 @@ namespace Core\API\Permission {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch API permissions");
}
}
class Save extends PermissionAPI {
@@ -192,5 +207,10 @@ namespace Core\API\Permission {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN],
"Allows users to modify API permissions. This is restricted to the administrator and cannot be changed");
}
}
}

View File

@@ -3,15 +3,10 @@
namespace Core\API;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\Context;
use PhpMqtt\Client\MqttClient;
/**
* TODO: we need following features, probably as abstract/generic class/method:
* - easy way for pagination (select with limit/offset)
* - CRUD Endpoints/Objects (Create, Update, Delete)
*/
abstract class Request {
protected Context $context;
@@ -62,7 +57,7 @@ abstract class Request {
}
}
protected function forbidMethod($method) {
protected function forbidMethod($method): void {
if (($key = array_search($method, $this->allowedMethods)) !== false) {
unset($this->allowedMethods[$key]);
}
@@ -76,7 +71,7 @@ abstract class Request {
return $this->isDisabled;
}
protected function allowMethod($method) {
protected function allowMethod($method): void {
$availableMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "TRACE", "CONNECT"];
if (in_array($method, $availableMethods) && !in_array($method, $this->allowedMethods)) {
$this->allowedMethods[] = $method;
@@ -113,7 +108,7 @@ abstract class Request {
return true;
}
public function parseVariableParams($values) {
public function parseVariableParams($values): void {
foreach ($values as $name => $value) {
if (isset($this->params[$name])) continue;
$type = Parameter\Parameter::parseType($value);
@@ -129,6 +124,7 @@ abstract class Request {
}
protected abstract function _execute(): bool;
public static function getDefaultACL(Insert $insert): void { }
public final function execute($values = array()): bool {
@@ -203,9 +199,11 @@ abstract class Request {
} else if ($session) {
$tfaToken = $session->getUser()->getTwoFactorToken();
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
$this->lastError = '2FA-Authorization is required';
http_response_code(401);
return false;
if (!($this instanceof \Core\API\Tfa\VerifyTotp) && !($this instanceof \Core\API\Tfa\VerifyKey)) {
$this->lastError = '2FA-Authorization is required';
http_response_code(401);
return false;
}
}
}
}
@@ -225,7 +223,7 @@ abstract class Request {
// Check for permission
if (!($this instanceof \Core\API\Permission\Save)) {
$req = new \Core\API\Permission\Check($this->context);
$this->success = $req->execute(array("method" => $this->getMethod()));
$this->success = $req->execute(array("method" => self::getEndpoint()));
$this->lastError = $req->getLastError();
if (!$this->success) {
return false;
@@ -266,11 +264,11 @@ abstract class Request {
}
protected function getParam($name, $obj = NULL): mixed {
// I don't know why phpstorm
if ($obj === NULL) {
$obj = $this->params;
}
// I don't know why phpstorm
return (isset($obj[$name]) ? $obj[$name]->value : NULL);
}
@@ -302,10 +300,30 @@ abstract class Request {
return $this->externalCall;
}
private function getMethod() {
$class = str_replace("\\", "/", get_class($this));
$class = substr($class, strlen("api/"));
return $class;
public static function getEndpoint(string $prefix = ""): ?string {
$reflectionClass = new \ReflectionClass(get_called_class());
if ($reflectionClass->isAbstract()) {
return null;
}
$isNestedAPI = $reflectionClass->getParentClass()->getName() !== Request::class;
if (!$isNestedAPI) {
# e.g. /api/stats or /api/info
$methodName = $reflectionClass->getShortName();
return $prefix . lcfirst($methodName);
} else {
# e.g. /api/user/login
$methodClass = $reflectionClass;
$nestedClass = $reflectionClass->getParentClass();
while (!endsWith($nestedClass->getName(), "API")) {
$methodClass = $nestedClass;
$nestedClass = $nestedClass->getParentClass();
}
$nestedAPI = substr(lcfirst($nestedClass->getShortName()), 0, -3);
$methodName = lcfirst($methodClass->getShortName());
return $prefix . $nestedAPI . "/" . $methodName;
}
}
public function getJsonResult(): string {
@@ -314,7 +332,7 @@ abstract class Request {
return json_encode($this->result);
}
protected function disableOutputBuffer() {
protected function disableOutputBuffer(): void {
ob_implicit_flush(true);
$levels = ob_get_level();
for ( $i = 0; $i < $levels; $i ++ ) {
@@ -323,7 +341,7 @@ abstract class Request {
flush();
}
protected function disableCache() {
protected function disableCache(): void {
header("Last-Modified: " . (new \DateTime())->format("D, d M Y H:i:s T"));
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
@@ -331,7 +349,7 @@ abstract class Request {
header("Pragma: no-cache");
}
protected function setupSSE() {
protected function setupSSE(): void {
$this->context->sendCookies();
$this->context->getSQL()?->close();
set_time_limit(0);
@@ -348,7 +366,7 @@ abstract class Request {
* @throws \PhpMqtt\Client\Exceptions\DataTransferException
* @throws \PhpMqtt\Client\Exceptions\MqttClientException
*/
protected function startMqttSSE(MqttClient $mqtt, callable $onPing) {
protected function startMqttSSE(MqttClient $mqtt, callable $onPing): void {
$lastPing = 0;
$mqtt->registerLoopEventHandler(function(MqttClient $mqtt, $elapsed) use (&$lastPing, $onPing) {
if ($elapsed - $lastPing >= 5) {
@@ -366,7 +384,7 @@ abstract class Request {
$mqtt->disconnect();
}
protected function processImageUpload(string $uploadDir, array $allowedExtensions = ["jpg","jpeg","png","gif"], $transformCallback = null) {
protected function processImageUpload(string $uploadDir, array $allowedExtensions = ["jpg","jpeg","png","gif"], $transformCallback = null): bool|array {
if (empty($_FILES)) {
return $this->createError("You need to upload an image.");
} else if (count($_FILES) > 1) {
@@ -470,4 +488,49 @@ abstract class Request {
return [$fileName, $files[$fileName]];
}
}
public static function getApiEndpoints(): array {
// first load all direct classes
$classes = [];
$apiDirs = ["Core", "Site"];
foreach ($apiDirs as $apiDir) {
$basePath = realpath(WEBROOT . "/$apiDir/API/");
if (!$basePath) {
continue;
}
foreach (scandir($basePath) as $fileName) {
$fullPath = $basePath . "/" . $fileName;
if (is_file($fullPath) && endsWith($fileName, ".class.php")) {
require_once $fullPath;
$apiName = explode(".", $fileName)[0];
$className = "\\$apiDir\\API\\$apiName";
if (!class_exists($className)) {
continue;
}
$reflectionClass = new \ReflectionClass($className);
if (!$reflectionClass->isSubclassOf(Request::class) || $reflectionClass->isAbstract()) {
continue;
}
$endpoint = "$className::getEndpoint"();
$classes[$endpoint] = $reflectionClass;
}
}
}
// then load all inheriting classes
foreach (get_declared_classes() as $declaredClass) {
$reflectionClass = new \ReflectionClass($declaredClass);
if (!$reflectionClass->isAbstract() && $reflectionClass->isSubclassOf(Request::class)) {
$className = $reflectionClass->getName();
$endpoint = "$className::getEndpoint"();
$classes[$endpoint] = $reflectionClass;
}
}
return $classes;
}
}

View File

@@ -69,8 +69,10 @@ namespace Core\API\Routes {
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;
@@ -101,6 +103,10 @@ namespace Core\API\Routes {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch site routing");
}
}
class Save extends RoutesAPI {
@@ -202,6 +208,10 @@ namespace Core\API\Routes {
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to save the site routing");
}
}
class Add extends RoutesAPI {
@@ -236,6 +246,10 @@ namespace Core\API\Routes {
$this->lastError = $sql->getLastError();
return $this->success && $this->regenerateCache();
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to add new routes");
}
}
class Update extends RoutesAPI {
@@ -270,9 +284,13 @@ namespace Core\API\Routes {
$exact = $this->getParam("exact");
$active = $this->getParam("active");
if ($route->getType() !== $type) {
$route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
if ($route === null) {
if (!$route->delete($sql)) {
return false;
} else {
$route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
if ($route === null) {
return false;
}
}
} else {
$route->setPattern($pattern);
@@ -286,6 +304,10 @@ namespace Core\API\Routes {
$this->lastError = $sql->getLastError();
return $this->success && $this->regenerateCache();
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to update existing routes");
}
}
class Remove extends RoutesAPI {
@@ -311,6 +333,10 @@ namespace Core\API\Routes {
$this->lastError = $sql->getLastError();
return $this->success && $this->regenerateCache();
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to remove routes");
}
}
class Enable extends RoutesAPI {
@@ -325,6 +351,10 @@ namespace Core\API\Routes {
$id = $this->getParam("id");
return $this->toggleRoute($id, true);
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to enable a route");
}
}
class Disable extends RoutesAPI {
@@ -339,6 +369,10 @@ namespace Core\API\Routes {
$id = $this->getParam("id");
return $this->toggleRoute($id, false);
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to disable a route");
}
}
class GenerateCache extends RoutesAPI {
@@ -380,6 +414,10 @@ namespace Core\API\Routes {
public function getRouter(): ?Router {
return $this->router;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to regenerate the routing cache");
}
}
}

View File

@@ -22,8 +22,10 @@ namespace Core\API\Settings {
use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\SQL\Condition\CondRegex;
use Core\Driver\SQL\Query\Insert;
use Core\Driver\SQL\Strategy\UpdateStrategy;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Group;
class Get extends SettingsAPI {
@@ -46,6 +48,10 @@ namespace Core\API\Settings {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch site settings");
}
}
class Set extends SettingsAPI {
@@ -144,6 +150,10 @@ namespace Core\API\Settings {
$this->lastError = $sql->getLastError();
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to modify site settings");
}
}
class GenerateJWT extends SettingsAPI {
@@ -173,5 +183,9 @@ namespace Core\API\Settings {
$this->result["jwt_public_key"] = $settings->getJwtPublicKey(false)?->getKeyMaterial();
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to regenerate the JWT key. This invalidates all sessions");
}
}
}

View File

@@ -4,6 +4,8 @@ namespace Core\API;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\Expression\Distinct;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\DatabaseEntity\Group;
use DateTime;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondBool;
@@ -113,4 +115,7 @@ class Stats extends Request {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to view site statistics");
}
}

View File

@@ -19,60 +19,11 @@ class Swagger extends Request {
die($this->getDocumentation());
}
private function getApiEndpoints(): array {
// first load all direct classes
$classes = [];
$apiDirs = ["Core", "Site"];
foreach ($apiDirs as $apiDir) {
$basePath = realpath(WEBROOT . "/$apiDir/API/");
if (!$basePath) {
continue;
}
foreach (scandir($basePath) as $fileName) {
$fullPath = $basePath . "/" . $fileName;
if (is_file($fullPath) && endsWith($fileName, ".class.php")) {
require_once $fullPath;
$apiName = explode(".", $fileName)[0];
$className = "\\$apiDir\\API\\$apiName";
if (!class_exists($className)) {
var_dump("Class not exist: $className");
continue;
}
$reflection = new \ReflectionClass($className);
if (!$reflection->isSubclassOf(Request::class) || $reflection->isAbstract()) {
continue;
}
$endpoint = "/" . strtolower($apiName);
$classes[$endpoint] = $reflection;
}
}
}
// then load all inheriting classes
foreach (get_declared_classes() as $declaredClass) {
$reflectionClass = new \ReflectionClass($declaredClass);
if (!$reflectionClass->isAbstract() && $reflectionClass->isSubclassOf(Request::class)) {
$inheritingClass = $reflectionClass->getParentClass();
if ($inheritingClass->isAbstract() && endsWith($inheritingClass->getShortName(), "API")) {
$endpoint = strtolower(substr($inheritingClass->getShortName(), 0, -3));
$endpoint = "/$endpoint/" . lcfirst($reflectionClass->getShortName());
$classes[$endpoint] = $reflectionClass;
}
}
}
return $classes;
}
private function fetchPermissions(): array {
$req = new Permission\Fetch($this->context);
$req = new \Core\API\Permission\Fetch($this->context);
$this->success = $req->execute();
$permissions = [];
foreach( $req->getResult()["permissions"] as $permission) {
foreach ($req->getResult()["permissions"] as $permission) {
$permissions["/" . strtolower($permission["method"])] = $permission["groups"];
}
@@ -85,12 +36,13 @@ class Swagger extends Request {
}
$currentUser = $this->context->getUser();
if (($request->loginRequired() || !empty($requiredGroups)) && !$currentUser) {
$isLoggedIn = $currentUser !== null;
if (($request->loginRequired() || !empty($requiredGroups)) && !$isLoggedIn) {
return false;
}
// special case: hardcoded permission
if ($request instanceof Permission\Save && (!$currentUser || !$currentUser->hasGroup(Group::ADMIN))) {
if ($request instanceof \Core\API\Permission\Save && (!$isLoggedIn || !$currentUser->hasGroup(Group::ADMIN))) {
return false;
}

View File

@@ -36,17 +36,20 @@ namespace Core\API {
return true;
}
protected function verifyClientDataJSON($jsonData, KeyBasedTwoFactorToken $token): bool {
protected function verifyClientDataJSON(array $jsonData, KeyBasedTwoFactorToken $token): bool {
$settings = $this->context->getSettings();
$expectedType = $token->isConfirmed() ? "webauthn.get" : "webauthn.create";
$type = $jsonData["type"] ?? "null";
if ($type !== $expectedType) {
return $this->createError("Invalid client data json type. Expected: '$expectedType', Got: '$type'");
} else if ($token->getData() !== base64url_decode($jsonData["challenge"] ?? "")) {
} else if (base64url_decode($token->getChallenge()) !== base64url_decode($jsonData["challenge"] ?? "")) {
return $this->createError("Challenge does not match");
} else if (($jsonData["origin"] ?? null) !== $settings->getBaseURL()) {
}
$origin = $jsonData["origin"] ?? null;
if ($origin !== $settings->getBaseURL()) {
$baseUrl = $settings->getBaseURL();
return $this->createError("Origin does not match. Expected: '$baseUrl', Got: '${$jsonData["origin"]}'");
// return $this->createError("Origin does not match. Expected: '$baseUrl', Got: '$origin'");
}
return true;
@@ -96,31 +99,34 @@ namespace Core\API\TFA {
if ($this->success && $token->isConfirmed()) {
// send an email
$settings = $this->context->getSettings();
$req = new \Core\API\Template\Render($this->context);
$this->success = $req->execute([
"file" => "mail/2fa_remove.twig",
"parameters" => [
"username" => $currentUser->getFullName() ?? $currentUser->getUsername(),
"site_name" => $settings->getSiteName(),
"sender_mail" => $settings->getMailSender()
]
]);
if ($this->success) {
$body = $req->getResult()["html"];
$gpg = $currentUser->getGPG();
$siteName = $settings->getSiteName();
$req = new \Core\API\Mail\Send($this->context);
$email = $currentUser->getEmail();
if ($email) {
$settings = $this->context->getSettings();
$req = new \Core\API\Template\Render($this->context);
$this->success = $req->execute([
"to" => $currentUser->getEmail(),
"subject" => "[$siteName] 2FA-Authentication removed",
"body" => $body,
"gpgFingerprint" => $gpg?->getFingerprint()
"file" => "mail/2fa_remove.twig",
"parameters" => [
"username" => $currentUser->getFullName() ?? $currentUser->getUsername(),
"site_name" => $settings->getSiteName(),
"sender_mail" => $settings->getMailSender()
]
]);
}
$this->lastError = $req->getLastError();
if ($this->success) {
$body = $req->getResult()["html"];
$gpg = $currentUser->getGPG();
$siteName = $settings->getSiteName();
$req = new \Core\API\Mail\Send($this->context);
$this->success = $req->execute([
"to" => $email,
"subject" => "[$siteName] 2FA-Authentication removed",
"body" => $body,
"gpgFingerprint" => $gpg?->getFingerprint()
]);
}
$this->lastError = $req->getLastError();
}
}
return $this->success;
@@ -211,6 +217,8 @@ namespace Core\API\TFA {
return $this->createError("Invalid 2FA-token endpoint");
}
$this->result["time"] = time();
$this->result["time_zone"] = $this->context->getSettings()->getTimeZone();
$code = $this->getParam("code");
if (!$twoFactorToken->verify($code)) {
return $this->createError("Code does not match");
@@ -250,11 +258,11 @@ namespace Core\API\TFA {
if (!($twoFactorToken instanceof KeyBasedTwoFactorToken) || $twoFactorToken->isConfirmed()) {
return $this->createError("You already added a two factor token");
} else {
$challenge = base64_encode($twoFactorToken->getData());
$challenge = $twoFactorToken->getChallenge();
}
} else {
$challenge = base64_encode(generateRandomString(32, "raw"));
$twoFactorToken = new KeyBasedTwoFactorToken($challenge);
$twoFactorToken = KeyBasedTwoFactorToken::create();
$challenge = $twoFactorToken->getChallenge();
$this->success = ($twoFactorToken->save($sql) !== false);
$this->lastError = $sql->getLastError();
if (!$this->success) {
@@ -262,7 +270,7 @@ namespace Core\API\TFA {
}
$currentUser->setTwoFactorToken($twoFactorToken);
$this->success = $currentUser->save($sql) !== false;
$this->success = $currentUser->save($sql, ["twoFactorToken"]) !== false;
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;

View File

@@ -8,15 +8,16 @@ use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityQuery;
use Core\Objects\DatabaseEntity\User;
trait Pagination {
function getPaginationParameters(array $orderColumns, string $defaultOrderBy = "id", string $defaultSortOrder = "asc"): array {
function getPaginationParameters(array $orderColumns, string $defaultOrderBy = null, string $defaultSortOrder = "asc"): array {
$this->paginationOrderColumns = $orderColumns;
$defaultOrderBy = $defaultOrderBy ?? current($orderColumns);
return [
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 25),
'orderBy' => new StringType('orderBy', -1, true, $defaultOrderBy, $orderColumns),
'sortOrder' => new StringType('sortOrder', -1, true, $defaultSortOrder, ['asc', 'desc']),
];

View File

@@ -138,6 +138,7 @@ namespace Core\API\User {
use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Expression\Alias;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\UserToken;
use Core\Driver\SQL\Column\Column;
@@ -182,6 +183,9 @@ 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));
@@ -206,6 +210,10 @@ namespace Core\API\User {
public function getUser(): User {
return $this->user;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to create new users");
}
}
class Fetch extends UserAPI {
@@ -263,6 +271,10 @@ namespace Core\API\User {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to fetch all users");
}
}
class Get extends UserAPI {
@@ -302,6 +314,10 @@ namespace Core\API\User {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to get details about a user");
}
}
class Info extends UserAPI {
@@ -314,31 +330,35 @@ namespace Core\API\User {
public function _execute(): bool {
$currentUser = $this->context->getUser();
$language = $this->context->getLanguage();
$this->result["language"] = $language->jsonSerialize();
if (!$currentUser) {
$this->result["loggedIn"] = false;
$userGroups = [];
} else {
$this->result["loggedIn"] = true;
$userGroups = array_keys($currentUser->getGroups());
$sql = $this->context->getSQL();
$res = $sql->select("method", "groups")
->from("ApiPermission")
->execute();
$permissions = [];
if (is_array($res)) {
foreach ($res as $row) {
$requiredGroups = json_decode($row["groups"], true);
if (empty($requiredGroups) || !empty(array_intersect($requiredGroups, $userGroups))) {
$permissions[] = $row["method"];
}
}
}
$this->result["permissions"] = $permissions;
$this->result["user"] = $currentUser->jsonSerialize();
$this->result["session"] = $this->context->getSession()->jsonSerialize();
}
$sql = $this->context->getSQL();
$res = $sql->select("method", "groups")
->from("ApiPermission")
->execute();
$this->result["permissions"] = [];
if (is_array($res)) {
foreach ($res as $row) {
$requiredGroups = json_decode($row["groups"], true);
if (empty($requiredGroups) || !empty(array_intersect($requiredGroups, $userGroups))) {
$this->result["permissions"][] = $row["method"];
}
}
}
return $this->success;
}
}
@@ -415,6 +435,10 @@ namespace Core\API\User {
$this->logger->info("Created new user with id=" . $user->getId());
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to invite new users");
}
}
class AcceptInvite extends UserAPI {
@@ -526,6 +550,14 @@ namespace Core\API\User {
if ($this->context->getUser()) {
$this->lastError = L('You are already logged in');
$this->success = true;
$tfaToken = $this->context->getUser()->getTwoFactorToken();
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
$this->result["twoFactorToken"] = $tfaToken->jsonSerialize([
"type", "challenge", "authenticated", "confirmed", "credentialID"
]);
}
return true;
}
@@ -552,16 +584,19 @@ namespace Core\API\User {
return $this->createError("Error creating Session: " . $sql->getLastError());
} else {
$tfaToken = $user->getTwoFactorToken();
$this->result["loggedIn"] = true;
$this->result["user"] = $user->jsonSerialize();
$this->result["session"] = $session->jsonSerialize();
$this->result["logoutIn"] = $session->getExpiresSeconds();
$this->result["csrfToken"] = $session->getCsrfToken();
if ($tfaToken && $tfaToken->isConfirmed()) {
$this->result["2fa"] = ["type" => $tfaToken->getType()];
if ($tfaToken instanceof KeyBasedTwoFactorToken) {
$challenge = base64_encode(generateRandomString(32, "raw"));
$this->result["2fa"]["challenge"] = $challenge;
$_SESSION["challenge"] = $challenge;
$tfaToken->generateChallenge();
}
$this->result["twoFactorToken"] = $tfaToken->jsonSerialize([
"type", "challenge", "authenticated", "confirmed", "credentialID"
]);
}
$this->success = true;
}
@@ -823,6 +858,10 @@ namespace Core\API\User {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to modify other user's details");
}
}
class Delete extends UserAPI {
@@ -856,6 +895,10 @@ namespace Core\API\User {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to delete other users");
}
}
class RequestPasswordReset extends UserAPI {

View File

@@ -17,6 +17,8 @@ namespace Core\API\Visitors {
use Core\API\Parameter\StringType;
use Core\API\VisitorsAPI;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\Query\Insert;
use Core\Objects\DatabaseEntity\Group;
use DateTime;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Expression\Add;
@@ -112,5 +114,9 @@ namespace Core\API\Visitors {
return $this->success;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to view visitor statistics");
}
}
}