1<?php
2/**
3 * Allow regular expressions in ACL entry objects
4 *
5 * Original idea raised in https://github.com/splitbrain/dokuwiki/issues/1957
6 *
7 * @license  GPL 2 (http://www.gnu.org/licenses/gpl.html)
8 * @author   Iain Hallam <iain@nineworlds.net>
9 */
10
11if(!defined('DOKU_INC')) die();
12
13class action_plugin_aclregex extends DokuWiki_Action_Plugin {
14
15  /**
16   * Register handlers with DokuWiki's event system
17   *
18   * @param  Doku_Event_Handler  $controller  DokuWiki's event controller object
19   *
20   * @return  not required
21   */
22  public function register(Doku_Event_Handler $controller) {
23    $controller->register_hook('AUTH_ACL_CHECK', 'BEFORE', $this, '_handle_aclregex_check');
24  }
25
26  /**
27   * Event handler run before AUTH_ACL_CHECK
28   *
29   * Modelled on DokuWiki's own auth_aclcheck_cb() in inc/auth.php
30   *
31   * @param  Doku_Event  $event  Event object by reference
32   * @param  mixed       $param  The parameters passed to register_hook
33   *
34   * @return  int  AUTH_<X>
35   */
36  public function _handle_aclregex_check(Doku_Event $event, $param) {
37    // Prevent default event to do our own auth check
38    $event->preventDefault();
39
40    // Access event data
41    $id     = $event->data['id'];     // @var string   $id
42    //dbg('Raw id: ' . $id);
43    $user   = $event->data['user'];   // @var string   $user
44    //dbg('Raw user: ' . $user);
45    $groups = $event->data['groups']; // @var string[] $groups
46    //dbg('Raw groups: ' . implode(', ', $groups));
47
48    // Access global variables
49    global $conf;     // @var string[]             $conf      The global configuration dictionary
50    global $AUTH_ACL; // @var string[]             $AUTH_ACL  Strings in the form <object>\t<subject>[ \t]+<permission>
51    //dbg('Raw ACLs:' . NL . implode(NL, $AUTH_ACL));
52    global $auth;     // @var DokuWiki_Auth_Plugin $auth      The global authentication handler
53
54    // If no ACL is used always return upload rights
55    if (! $conf['useacl']) {
56      //dbg('Not configured for ACLs');
57      $event->result = AUTH_UPLOAD;
58      return AUTH_UPLOAD;
59    }
60
61    // If no auth is loaded return no rights
62    if (! $auth) {
63      //dbg('No auth backend loaded');
64      $event->result = AUTH_NONE;
65      return AUTH_NONE;
66    }
67
68    // If no ACLs exist return no rights
69    if (! count($AUTH_ACL)) {
70      msg('No ACL setup yet! Denying access to everyone.');
71      $event->result = AUTH_NONE;
72      return AUTH_NONE;
73    }
74
75    // Make sure $groups is an array
76    if (! is_array($groups)) $groups = array();
77
78    // If user is superuser or in superuser group return admin rights
79    if (auth_isadmin($user, $groups)) {
80      //dbg('Admin user');
81      $event->result = AUTH_ADMIN;
82      return AUTH_ADMIN;
83    }
84
85    // Clean up user name (and encode any special characters) and groups
86    if (! $auth->isCaseSensitive()) {
87      $user   = utf8_strtolower($user);
88      $groups = array_map('utf8_strtolower', $groups);
89    }
90    $user   = auth_nameencode($auth->cleanUser($user));
91    $groups = array_map(array($auth, 'cleanGroup'), (array) $groups);
92
93    // Make sure groups start with @ and encode any special characters
94    foreach ($groups as &$group) {
95      $group = '@'.auth_nameencode($group);
96    }
97
98    // Add @ALL group
99    $groups[] = '@ALL';
100
101    // Add user to match against
102    if ($user) $groups[] = $user;
103    //dbg('processed user and groups: ' . implode(', ', $groups));
104
105    // Set initial search variables
106    $highest_permission = -1;
107
108    // Build ACL parts structure
109    $acl_parts_list = array();
110    foreach ($AUTH_ACL as $acl) {
111      // Ignore comments
112      $acl = preg_replace('/#.*$/', '', $acl);
113      //dbg('Processing ACL: ' . $acl);
114
115      // Access ACL parts
116      list($acl_object, $acl_subject, $acl_permission, $acl_rest) = preg_split('/[ \t]+/', $acl, 4);
117      //dbg('Object: ' . $acl_object);
118      //dbg('Subject: ' . $acl_subject);
119      //dbg('Permission: ' . $acl_permission);
120      //dbg('Rest: ' . $acl_rest);
121
122      // Quote ACL object parts that aren't in a regex to treat them literally
123      $acl_object_parts = explode('/', $acl_object); // Split on delimiters
124      $acl_object = ''; // Rebuild $acl_object from scratch
125      foreach ($acl_object_parts as $key => $part) {
126        if ($key % 2 == 0) { // Only for even keys counting from 0, i.e., 1st, 3rd, 5th, etc., which should be the parts outside / delimiters
127          $part = preg_quote($part); // Quote any PCRE special characters
128        }
129
130        $acl_object .= $part; // Add back to $acl_object
131      }
132      $acl_object = '|^' . $acl_object . '$|'; // Add PCRE delimiters to resulting $acl_object
133      //dbg('Quoted object: ' . $acl_object);
134
135      // Assign into the ACL parts structure
136      $acl_parts_list[] = array(
137        'object'     => $acl_object,
138        'subject'    => $acl_subject,
139        'permission' => $acl_permission,
140        'rest'       => $acl_rest
141      );
142    }
143
144    // Check for exact object matches
145    foreach ($acl_parts_list as $acl_parts) {
146      if (preg_match($acl_parts['object'], $id)) {
147        //dbg('Matched ID ' . $id . ' with search string ' . $acl_parts['object']);
148
149        $line_permission = $this->_check_permission($groups, $acl_parts['subject'], $acl_parts['permission']);
150        //dbg('Line permission returned: ' . $line_permission);
151
152        // The highest permission found is what gets returned
153        if ($line_permission > $highest_permission) {
154          //dbg('New highest permission: ' . $line_permission);
155          $highest_permission = $line_permission;
156        }
157      }
158    }
159
160    // If we had a match return it
161    if ($highest_permission > -1) {
162      //dbg('Permission: ' . $highest_permission);
163      $event->result = $highest_permission;
164      return $highest_permission;
165    }
166
167    // There wasn't an exact match, so check up the tree for namespace matches
168    //dbg('No permissions from exact matches - trying namespaces');
169
170    // Set path match string using namespace
171    $ns = getNS($id);
172    $path = $ns . ':*';
173    if ($path == ':*') $path = '*'; // $id is in the root namespace
174    //dbg('Path: ' . $path);
175
176    // Loop to work our way up the tree if there's no match first time round
177    do {
178      foreach ($acl_parts_list as $acl_parts) {
179        if (preg_match($acl_parts['object'], $path)) {
180          //dbg('Matched namespace path ' . $path . ' with search string ' . $acl_parts['object']);
181
182          $line_permission = $this->_check_permission($groups, $acl_parts['subject'], $acl_parts['permission']);
183          //dbg('Line permission returned: ' . $line_permission);
184
185          // The highest permission found is what gets returned
186          if ($line_permission > $highest_permission) {
187            //dbg('New highest permission: ' . $line_permission);
188            $highest_permission = $line_permission;
189          }
190        }
191      }
192
193      // If we had a match return it
194      if ($highest_permission > -1) {
195        //dbg('Permission: ' . $highest_permission);
196        $event->result = $highest_permission;
197        return $highest_permission;
198      }
199
200      // If we're not already at the root, get the next higher namespace
201      if ($path != '*') {
202        $ns = getNS($ns);
203        $path = $ns . ':*';
204        if ($path == ':*') $path = '*';
205        //dbg('Next path: ' . $path);
206      } else {
207        // We were at the root already but didn't get a match; move on to the next ACL
208        msg('No ACL setup yet! Denying access to everyone.');
209        break;
210      }
211    } while (true); // This shouldn't be endless as there are exit conditions in the loop
212
213    // No matches = no permission
214    //dbg('Permission: ' . AUTH_NONE);
215    $event->result = AUTH_NONE;
216    return AUTH_NONE;
217  }
218
219  /**
220   * Check the resulting permission for user's groups and username
221   *
222   * @param  string[]              $groups          A list of groups (including the username) to check
223   * @param  string                $acl_subject     The subject (user or group) assigned in the ACL
224   * @param  int                   $acl_permission  The permission assigned in the ACL
225   */
226  private function _check_permission($groups, $acl_subject, $acl_permission) {
227    // Access global variables
228    global $auth;     // @var DokuWiki_Auth_Plugin $auth      The global authentication handler
229
230    //dbg('_check_permission $groups: ' . $groups);
231    //dbg('_check_permission $acl_subject: ' . $acl_subject);
232    //dbg('_check_permission $acl_permission: ' . $acl_permission);
233
234    // Lowercase acl_subject except @ALL if we can
235    if (! $auth->isCaseSensitive() && $acl_subject !== '@ALL') {
236      $acl_subject = utf8_strtolower($acl_subject);
237    }
238
239    // If $acl_subject doesn't contain one of the user's groups or their user name, move on
240    // This would be where to change the plugin to support regexes in subjects
241    if(! in_array($acl_subject, $groups)) {
242      return -1;
243    }
244
245    // Don't allow admin permissions from ACLs!
246    if ($acl_permission > AUTH_DELETE) $acl_permission = AUTH_DELETE;
247
248    return $acl_permission;
249  }
250}
251