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