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 
11 if(!defined('DOKU_INC')) die();
12 
13 class 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