From 64cdf7793cbaf7c5b5e6cc05ef184827b5efa5ec Mon Sep 17 00:00:00 2001
From: Andreas Gohr <gohr@cosmocode.de>
Date: Tue, 30 Oct 2018 14:07:15 +0100
Subject: [PATCH] add event to check access to admin plugins

This adds a new method that capsulates the access check that has to be
done to decide if an admin plugin's page should be shown to the user.
The default implementation is the same as before, relying only on the
forAdminOnly() method and the users' isadmin or ismanager status.

Admin plugins themselves can override the method to do additional
checks. In this patch, I added that to the usermanager plugin which will
only return true if the current auth backend can list users.

However the real idea behind this change is that the new method emits a
new event called ADMINPLUGIN_ACCESS_CHECK which would allow plugins to
overwrite it. This way it could be possible to give certain user groups
access to certain admin plugins without giving them admin or manager
permissions.

Note: this does not change how the "Admin" link is shown, it still
depends on ismanager or isadmin. A plugin as mentioned above would need
to influence the display via the MENU_ITEMS_ASSEMBLY event.

Note: this only covers the basic access check. Admin plugins may need
further adjustments for access to other parts of the plugin (like AJAX
components). An additional commit will update this for the bundled
plugins.
---
 inc/Action/Admin.php              |   2 +-
 inc/Ui/Admin.php                  | 112 ++++++++++++++----------------
 inc/pluginutils.php               |   2 +-
 lib/plugins/admin.php             |  23 ++++++
 lib/plugins/usermanager/admin.php |  18 +++++
 5 files changed, 96 insertions(+), 61 deletions(-)

diff --git a/inc/Action/Admin.php b/inc/Action/Admin.php
index 8d4305788..eedf7174c 100644
--- a/inc/Action/Admin.php
+++ b/inc/Action/Admin.php
@@ -41,7 +41,7 @@ class Admin extends AbstractUserAction {
         if(($page = $INPUT->str('page', '', true)) != '') {
             /** @var $plugin \DokuWiki_Admin_Plugin */
             if($plugin = plugin_getRequestAdminPlugin()) { // FIXME this method does also permission checking
-                if($plugin->forAdminOnly() && !$INFO['isadmin']) {
+                if(!$plugin->isAccessibleByCurrentUser()) {
                     throw new ActionException('denied');
                 }
                 $plugin->handle();
diff --git a/inc/Ui/Admin.php b/inc/Ui/Admin.php
index aa3b8b99e..20416e137 100644
--- a/inc/Ui/Admin.php
+++ b/inc/Ui/Admin.php
@@ -12,6 +12,9 @@ namespace dokuwiki\Ui;
  */
 class Admin extends Ui {
 
+    protected $forAdmins = array('usermanager', 'acl', 'extension', 'config', 'styling');
+    protected $forManagers = array('revert', 'popularity');
+    /** @var array[] */
     protected $menu;
 
     /**
@@ -24,58 +27,30 @@ class Admin extends Ui {
         echo '<div class="ui-admin">';
         echo p_locale_xhtml('admin');
         $this->showSecurityCheck();
-        $this->showAdminMenu();
-        $this->showManagerMenu();
+        $this->showMenu('admin');
+        $this->showMenu('manager');
         $this->showVersion();
-        $this->showPluginMenu();
+        $this->showMenu('other');
         echo '</div>';
     }
 
     /**
-     * Display the standard admin tasks
+     * Show the given menu of available plugins
+     *
+     * @param string $type admin|manager|other
      */
-    protected function showAdminMenu() {
-        /** @var \DokuWiki_Auth_Plugin $auth */
-        global $auth;
-        global $INFO;
-
-        if(!$INFO['isadmin']) return;
-
-        // user manager only if the auth backend supports it
-        if(!$auth || !$auth->canDo('getUsers') ) {
-            if(isset($this->menu['usermanager'])) unset($this->menu['usermanager']);
+    protected function showMenu($type) {
+        if (!$this->menu[$type]) return;
+
+        if ($type === 'other') {
+            echo p_locale_xhtml('adminplugins');
+            $class = 'admin_plugins';
+        } else {
+            $class = 'admin_tasks';
         }
 
-        echo '<ul class="admin_tasks">';
-        foreach(array('usermanager','acl', 'extension', 'config', 'styling') as $plugin) {
-            if(!isset($this->menu[$plugin])) continue;
-            $this->showMenuItem($this->menu[$plugin]);
-            unset($this->menu[$plugin]);
-        }
-        echo '</ul>';
-    }
-
-    /**
-     * Display the standard manager tasks
-     */
-    protected function showManagerMenu() {
-        echo '<ul class="admin_tasks">';
-        foreach(array('revert','popularity') as $plugin) {
-            if(!isset($this->menu[$plugin])) continue;
-            $this->showMenuItem($this->menu[$plugin]);
-            unset($this->menu[$plugin]);
-        }
-        echo '</ul>';
-    }
-
-    /**
-     * Display all the remaining plugins
-     */
-    protected function showPluginMenu() {
-        if(!count($this->menu)) return;
-        echo p_locale_xhtml('adminplugins');
-        echo '<ul class="admin_plugins">';
-        foreach ($this->menu as $item) {
+        echo "<ul class=\"$class\">";
+        foreach ($this->menu[$type] as $item) {
             $this->showMenuItem($item);
         }
         echo '</ul>';
@@ -104,7 +79,9 @@ class Admin extends Ui {
         if(substr($conf['savedir'], 0, 2) !== './') return;
         echo '<a style="border:none; float:right;"
                 href="http://www.dokuwiki.org/security#web_access_security">
-                <img src="' . DOKU_URL . $conf['savedir'] . '/dont-panic-if-you-see-this-in-your-logs-it-means-your-directory-permissions-are-correct.png" alt="Your data directory seems to be protected properly."
+                <img src="' . DOKU_URL . $conf['savedir'] .
+                '/dont-panic-if-you-see-this-in-your-logs-it-means-your-directory-permissions-are-correct.png" 
+                alt="Your data directory seems to be protected properly."
                 onerror="this.parentNode.style.display=\'none\'" /></a>';
     }
 
@@ -136,19 +113,27 @@ class Admin extends Ui {
      * @return array list of plugins with their properties
      */
     protected function getPluginList() {
-        global $INFO;
         global $conf;
 
         $pluginlist = plugin_list('admin');
-        $menu = array();
+        $menu = ['admin' => [], 'manager' => [], 'other' => []];
+
         foreach($pluginlist as $p) {
             /** @var \DokuWiki_Admin_Plugin $obj */
-            if(($obj = plugin_load('admin', $p)) === null) continue;
+            if (($obj = plugin_load('admin', $p)) === null) continue;
 
             // check permissions
-            if($obj->forAdminOnly() && !$INFO['isadmin']) continue;
+            if (!$obj->isAccessibleByCurrentUser()) continue;
+
+            if (in_array($p, $this->forAdmins, true)) {
+                $type = 'admin';
+            } elseif (in_array($p, $this->forManagers, true)){
+                $type = 'manager';
+            } else {
+                $type = 'other';
+            }
 
-            $menu[$p] = array(
+            $menu[$type][$p] = array(
                 'plugin' => $p,
                 'prompt' => $obj->getMenuText($conf['lang']),
                 'icon' => $obj->getMenuIcon(),
@@ -157,17 +142,26 @@ class Admin extends Ui {
         }
 
         // sort by name, then sort
-        uasort(
-            $menu,
-            function ($a, $b) {
-                $strcmp = strcasecmp($a['prompt'], $b['prompt']);
-                if($strcmp != 0) return $strcmp;
-                if($a['sort'] == $b['sort']) return 0;
-                return ($a['sort'] < $b['sort']) ? -1 : 1;
-            }
-        );
+        uasort($menu['admin'], [$this, 'menuSort']);
+        uasort($menu['manager'], [$this, 'menuSort']);
+        uasort($menu['other'], [$this, 'menuSort']);
 
         return $menu;
     }
 
+    /**
+     * Custom sorting for admin menu
+     *
+     * We sort alphabetically first, then by sort value
+     *
+     * @param array $a
+     * @param array $b
+     * @return int
+     */
+    protected function menuSort ($a, $b) {
+        $strcmp = strcasecmp($a['prompt'], $b['prompt']);
+        if($strcmp != 0) return $strcmp;
+        if($a['sort'] === $b['sort']) return 0;
+        return ($a['sort'] < $b['sort']) ? -1 : 1;
+    }
 }
diff --git a/inc/pluginutils.php b/inc/pluginutils.php
index a395be435..0cd113b14 100644
--- a/inc/pluginutils.php
+++ b/inc/pluginutils.php
@@ -123,7 +123,7 @@ function plugin_getRequestAdminPlugin(){
                 /** @var $admin_plugin DokuWiki_Admin_Plugin */
                 $admin_plugin = plugin_load('admin', $page);
                 // verify
-                if ($admin_plugin && $admin_plugin->forAdminOnly() && !$INFO['isadmin']) {
+                if ($admin_plugin && !$admin_plugin->isAccessibleByCurrentUser()) {
                     $admin_plugin = null;
                     $INPUT->remove('page');
                     msg('For admins only',-1);
diff --git a/lib/plugins/admin.php b/lib/plugins/admin.php
index 4e1cbbb33..770fe582f 100644
--- a/lib/plugins/admin.php
+++ b/lib/plugins/admin.php
@@ -72,6 +72,29 @@ class DokuWiki_Admin_Plugin extends DokuWiki_Plugin {
         trigger_error('html() not implemented in '.get_class($this), E_USER_WARNING);
     }
 
+    /**
+     * Checks if access should be granted to this admin plugin
+     *
+     * @return bool true if the current user may access this admin plugin
+     */
+    public function isAccessibleByCurrentUser() {
+        global $INFO;
+
+        $data['hasAccess'] = false;
+
+        $event = new Doku_Event('ADMINPLUGIN_ACCESS_CHECK', $data);
+        if($event->advise_before()) {
+            if ($this->forAdminOnly()) {
+                $data['hasAccess'] = $INFO['isadmin'];
+            } else {
+                $data['hasAccess'] = $INFO['ismanager'];
+            }
+        }
+        $event->advise_after();
+
+        return $data['hasAccess'];
+    }
+
     /**
      * Return true for access only by admins (config:superuser) or false if managers are allowed as well
      *
diff --git a/lib/plugins/usermanager/admin.php b/lib/plugins/usermanager/admin.php
index 6d9bf3b20..3148971ce 100644
--- a/lib/plugins/usermanager/admin.php
+++ b/lib/plugins/usermanager/admin.php
@@ -297,6 +297,24 @@ class admin_plugin_usermanager extends DokuWiki_Admin_Plugin {
         return true;
     }
 
+    /**
+     * User Manager is only available if the auth backend supports it
+     *
+     * @inheritdoc
+     * @return bool
+     */
+    public function isAccessibleByCurrentUser()
+    {
+        /** @var DokuWiki_Auth_Plugin $auth */
+        global $auth;
+        if(!$auth || !$auth->canDo('getUsers') ) {
+            return false;
+        }
+
+        return parent::isAccessibleByCurrentUser();
+    }
+
+
     /**
      * Display form to add or modify a user
      *
-- 
GitLab