From bad08af314f871d758c3a7b0e0fa2c42e4c53571 Mon Sep 17 00:00:00 2001 From: Roman Hergenreder Date: Sat, 27 Jun 2020 01:18:10 +0200 Subject: [PATCH] Dynamic Permissions --- core/Api/PermissionAPI.class.php | 157 ++++++++++++++++++++ core/Api/Request.class.php | 47 ++++-- core/Api/SendTestMail.class.php | 2 + core/Configuration/CreateDatabase.class.php | 37 +++++ core/Driver/SQL/MySQL.class.php | 4 + 5 files changed, 233 insertions(+), 14 deletions(-) create mode 100644 core/Api/PermissionAPI.class.php diff --git a/core/Api/PermissionAPI.class.php b/core/Api/PermissionAPI.class.php new file mode 100644 index 0000000..52c73a4 --- /dev/null +++ b/core/Api/PermissionAPI.class.php @@ -0,0 +1,157 @@ +user->isLoggedIn() || !$this->user->hasGroup(USER_GROUP_ADMIN)) { + return $this->createError("Permission denied."); + } + + return true; + } + } +} + +namespace Api\Permission { + + use Api\Parameter\Parameter; + use Api\Parameter\StringType; + use Api\PermissionAPI; + use Driver\SQL\Condition\Compare; + use Objects\User; + + class Check extends PermissionAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + 'method' => new StringType('method', 323) + )); + + $this->isPublic = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $method = $this->getParam("method"); + $sql = $this->user->getSQL(); + $res = $sql->select("groups") + ->from("ApiPermission") + ->where(new Compare("method", $method)) + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + if (empty($res)) { + return true; + } + + $groups = json_decode($res[0]["groups"]); + if (empty($groups)) { + return true; + } + + if (!$this->user->isLoggedIn() || empty(array_intersect($groups, array_keys($this->user->getGroups())))) { + return $this->createError("Permission denied."); + } + } + + return $this->success; + } + } + + class Fetch extends PermissionAPI { + + private array $groups; + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array()); + } + + private function fetchGroups() { + $sql = $this->user->getSQL(); + $res = $sql->select("uid", "name") + ->from("Group") + ->orderBy("uid") + ->ascending() + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + $this->groups = array(); + foreach($res as $row) { + $groupId = $row["uid"]; + $groupName = $row["name"]; + $this->groups[$groupId] = $groupName; + } + } + + return $this->success; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + if (!$this->checkStaticPermission()) { + return false; + } + + if (!$this->fetchGroups()) { + return false; + } + + $sql = $this->user->getSQL(); + $res = $sql->select("method", "groups") + ->from("ApiPermission") + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + $permissions = array(); + foreach ($res as $row) { + $method = $row["method"]; + $groups = json_decode($row["groups"]); + $permissions[] = array("method" => $method, "groups" => $groups); + } + $this->result["permissions"] = $permissions; + $this->result["groups"] = $this->groups; + } + + return $this->success; + } + } + + class Save extends PermissionAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + 'permissions' => new Parameter('permissions', Parameter::TYPE_ARRAY) + )); + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + if (!$this->checkStaticPermission()) { + return false; + } + + + + return $this->success; + } + } +} \ No newline at end of file diff --git a/core/Api/Request.class.php b/core/Api/Request.class.php index e62a507..0e0f55d 100644 --- a/core/Api/Request.class.php +++ b/core/Api/Request.class.php @@ -121,23 +121,25 @@ class Request { } // TODO: Check this! - if($this->externalCall && ($this->loginRequired || !empty($this->requiredGroup))) { + if($this->externalCall) { $apiKeyAuthorized = false; - if(isset($values['api_key']) && $this->apiKeyAllowed) { - $apiKey = $values['api_key']; - $apiKeyAuthorized = $this->user->authorize($apiKey); + + // Logged in or api key authorized? + if ($this->loginRequired) { + if(isset($values['api_key']) && $this->apiKeyAllowed) { + $apiKey = $values['api_key']; + $apiKeyAuthorized = $this->user->authorize($apiKey); + } + + if(!$this->user->isLoggedIn() && !$apiKeyAuthorized) { + $this->lastError = 'You are not logged in.'; + header('HTTP 1.1 401 Unauthorized'); + return false; + } } - if(!$this->user->isLoggedIn() && !$apiKeyAuthorized) { - $this->lastError = 'You are not logged in.'; - header('HTTP 1.1 401 Unauthorized'); - return false; - } else if(!empty($this->requiredGroup) && empty(array_intersect($this->requiredGroup, array_keys($this->user->getGroups())))) { - $this->lastError = "Insufficient permissions. Required group: " - . implode(", ", array_map(function ($group) { return GroupName($group); }, $this->requiredGroup)); - header('HTTP 1.1 401 Unauthorized'); - return false; - } else if($this->csrfTokenRequired && !$apiKeyAuthorized && $this->externalCall) { + // CSRF Token + if($this->csrfTokenRequired && !$apiKeyAuthorized) { // csrf token required + external call // if it's not a call with API_KEY, check for csrf_token if (!isset($values["csrf_token"]) || strcmp($values["csrf_token"], $this->user->getSession()->getCsrfToken()) !== 0) { @@ -146,6 +148,17 @@ class Request { return false; } } + + // Check for permission + if (!($this instanceof PermissionAPI)) { + $req = new \Api\Permission\Check($this->user); + $this->success = $req->execute(array("method" => $this->getMethod())); + $this->lastError = $req->getLastError(); + if (!$this->success) { + header('HTTP 1.1 401 Unauthorized'); + return false; + } + } } if(!$this->parseParams($values)) @@ -181,6 +194,12 @@ class Request { public function loginRequired() { return $this->loginRequired; } public function isExternalCall() { return $this->externalCall; } + private function getMethod() { + $class = str_replace("\\", "/", get_class($this)); + $class = substr($class, strlen("api/")); + return $class; + } + public function getJsonResult() { $this->result['success'] = $this->success; $this->result['msg'] = $this->lastError; diff --git a/core/Api/SendTestMail.class.php b/core/Api/SendTestMail.class.php index 3276d7d..38cc192 100644 --- a/core/Api/SendTestMail.class.php +++ b/core/Api/SendTestMail.class.php @@ -11,6 +11,8 @@ class SendTestMail extends Request { parent::__construct($user, $externalCall, array( "receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL) )); + + $this->requiredGroup = array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT); } public function execute($values = array()) { diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index d56bd26..d0eb429 100755 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -171,6 +171,43 @@ class CreateDatabase { ->addDateTime("created_at", false, $sql->currentTimestamp()) ->primaryKey("uid"); + $queries[] = $sql->createTable("ApiPermission") + ->addString("method", 32) + ->addJson("groups", true, '[]') + ->primaryKey("method"); + + $queries[] = $sql->insert("ApiPermission", array("method", "groups")) + ->addRow("ApiKey/create", array()) + ->addRow("ApiKey/fetch", array()) + ->addRow("ApiKey/refresh", array()) + ->addRow("ApiKey/revoke", array()) + ->addRow("Contact/request", array()) + ->addRow("Groups/fetch", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN)) + ->addRow("Groups/create", array(USER_GROUP_ADMIN)) + ->addRow("Groups/delete", array(USER_GROUP_ADMIN)) + ->addRow("Language/get", array()) + ->addRow("Language/set", array()) + ->addRow("Notifications/create", array(USER_GROUP_ADMIN)) + ->addRow("Notifications/fetch", array()) + ->addRow("Notifications/seen", array()) + ->addRow("Routes/fetch", array(USER_GROUP_ADMIN)) + ->addRow("Routes/save", array(USER_GROUP_ADMIN)) + ->addRow("sendTestMail", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN)) + ->addRow("Settings/get", array(USER_GROUP_ADMIN)) + ->addRow("Settings/set", array(USER_GROUP_ADMIN)) + ->addRow("Stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT)) + ->addRow("User/create", array(USER_GROUP_ADMIN)) + ->addRow("User/fetch", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT)) + ->addRow("User/get", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT)) + ->addRow("User/info", array()) + ->addRow("User/invite", array(USER_GROUP_ADMIN)) + ->addRow("User/login", array()) + ->addRow("User/logout", array()) + ->addRow("User/register", array()) + ->addRow("User/checkToken", array()) + ->addRow("User/edit", array(USER_GROUP_ADMIN)) + ->addRow("User/delete", array(USER_GROUP_ADMIN)); + return $queries; } diff --git a/core/Driver/SQL/MySQL.class.php b/core/Driver/SQL/MySQL.class.php index 2a5fc26..9190274 100644 --- a/core/Driver/SQL/MySQL.class.php +++ b/core/Driver/SQL/MySQL.class.php @@ -102,6 +102,10 @@ class MySQL extends SQL { $value = $value->format('Y-m-d H:i:s'); $sqlParams[0] .= 's'; break; + case Parameter::TYPE_ARRAY: + $value = json_encode($value); + $sqlParams[0] .= 's'; + break; case Parameter::TYPE_EMAIL: default: $sqlParams[0] .= 's';