1<?php 2/** 3 * Plugin auth provider 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Jan Schumann <js@schumann-it.com> 7 */ 8// must be run within Dokuwiki 9if(!defined('DOKU_INC')) die(); 10 11require_once(DOKU_INC.'inc/adLDAP.php'); 12 13/** 14 * Active Directory authentication backend for DokuWiki 15 * 16 * This makes authentication with a Active Directory server much easier 17 * than when using the normal LDAP backend by utilizing the adLDAP library 18 * 19 * Usage: 20 * Set DokuWiki's local.protected.php auth setting to read 21 * 22 * $conf['useacl'] = 1; 23 * $conf['disableactions'] = 'register'; 24 * $conf['autopasswd'] = 0; 25 * $conf['authtype'] = 'authad'; 26 * $conf['passcrypt'] = 'ssha'; 27 * 28 * $conf['plugin']['authad']['account_suffix'] = '@my.domain.org'; 29 * $conf['plugin']['authad']['base_dn'] = 'DC=my,DC=domain,DC=org'; 30 * $conf['plugin']['authad']['domain_controllers'] = 'srv1.domain.org,srv2.domain.org'; 31 * 32 * //optional: 33 * $conf['plugin']['authad']['sso'] = 1; 34 * $conf['plugin']['authad']['ad_username'] = 'root'; 35 * $conf['plugin']['authad']['ad_password'] = 'pass'; 36 * $conf['plugin']['authad']['real_primarygroup'] = 1; 37 * $conf['plugin']['authad']['use_ssl'] = 1; 38 * $conf['plugin']['authad']['use_tls'] = 1; 39 * $conf['plugin']['authad']['debug'] = 1; 40 * 41 * // get additional information to the userinfo array 42 * // add a list of comma separated ldap contact fields. 43 * $conf['plugin']['authad']['additional'] = 'field1,field2'; 44 * 45 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 46 * @author James Van Lommel <jamesvl@gmail.com> 47 * @link http://www.nosq.com/blog/2005/08/ldap-activedirectory-and-dokuwiki/ 48 * @author Andreas Gohr <andi@splitbrain.org> 49 * @author Jan Schumann <js@schumann-it.com> 50 */ 51class auth_plugin_authad extends DokuWiki_Auth_Plugin 52{ 53 var $cnf = null; 54 var $opts = null; 55 var $adldap = null; 56 var $users = null; 57 58 /** 59 * Constructor 60 */ 61 function auth_plugin_authad() { 62 global $conf; 63 $this->cnf = $conf['auth']['ad']; 64 65 66 // additional information fields 67 if (isset($this->cnf['additional'])) { 68 $this->cnf['additional'] = str_replace(' ', '', $this->cnf['additional']); 69 $this->cnf['additional'] = explode(',', $this->cnf['additional']); 70 } else $this->cnf['additional'] = array(); 71 72 // ldap extension is needed 73 if (!function_exists('ldap_connect')) { 74 if ($this->cnf['debug']) 75 msg("AD Auth: PHP LDAP extension not found.",-1); 76 $this->success = false; 77 return; 78 } 79 80 // Prepare SSO 81 if($_SERVER['REMOTE_USER'] && $this->cnf['sso']){ 82 // remove possible NTLM domain 83 list($dom,$usr) = explode('\\',$_SERVER['REMOTE_USER'],2); 84 if(!$usr) $usr = $dom; 85 86 // remove possible Kerberos domain 87 list($usr,$dom) = explode('@',$usr); 88 89 $dom = strtolower($dom); 90 $_SERVER['REMOTE_USER'] = $usr; 91 92 // we need to simulate a login 93 if(empty($_COOKIE[DOKU_COOKIE])){ 94 $_REQUEST['u'] = $_SERVER['REMOTE_USER']; 95 $_REQUEST['p'] = 'sso_only'; 96 } 97 } 98 99 // prepare adLDAP standard configuration 100 $this->opts = $this->cnf; 101 102 // add possible domain specific configuration 103 if($dom && is_array($this->cnf[$dom])) foreach($this->cnf[$dom] as $key => $val){ 104 $this->opts[$key] = $val; 105 } 106 107 // handle multiple AD servers 108 $this->opts['domain_controllers'] = explode(',',$this->opts['domain_controllers']); 109 $this->opts['domain_controllers'] = array_map('trim',$this->opts['domain_controllers']); 110 $this->opts['domain_controllers'] = array_filter($this->opts['domain_controllers']); 111 112 // we can change the password if SSL is set 113 if($this->opts['use_ssl'] || $this->opts['use_tls']){ 114 $this->cando['modPass'] = true; 115 } 116 $this->cando['modName'] = true; 117 $this->cando['modMail'] = true; 118 } 119 120 /** 121 * Check user+password [required auth function] 122 * 123 * Checks if the given user exists and the given 124 * plaintext password is correct by trying to bind 125 * to the LDAP server 126 * 127 * @author James Van Lommel <james@nosq.com> 128 * @return bool 129 */ 130 function checkPass($user, $pass){ 131 if($_SERVER['REMOTE_USER'] && 132 $_SERVER['REMOTE_USER'] == $user && 133 $this->cnf['sso']) return true; 134 135 if(!$this->_init()) return false; 136 return $this->adldap->authenticate($user, $pass); 137 } 138 139 /** 140 * Return user info [required auth function] 141 * 142 * Returns info about the given user needs to contain 143 * at least these fields: 144 * 145 * name string full name of the user 146 * mail string email address of the user 147 * grps array list of groups the user is in 148 * 149 * This LDAP specific function returns the following 150 * addional fields: 151 * 152 * dn string distinguished name (DN) 153 * uid string Posix User ID 154 * 155 * @author James Van Lommel <james@nosq.com> 156 */ 157 function getUserData($user){ 158 global $conf; 159 if(!$this->_init()) return false; 160 161 $fields = array('mail','displayname','samaccountname'); 162 163 // add additional fields to read 164 $fields = array_merge($fields, $this->cnf['additional']); 165 $fields = array_unique($fields); 166 167 //get info for given user 168 $result = $this->adldap->user_info($user, $fields); 169 //general user info 170 $info['name'] = $result[0]['displayname'][0]; 171 $info['mail'] = $result[0]['mail'][0]; 172 $info['uid'] = $result[0]['samaccountname'][0]; 173 $info['dn'] = $result[0]['dn']; 174 175 // additional information 176 foreach ($this->cnf['additional'] as $field) { 177 if (isset($result[0][strtolower($field)])) { 178 $info[$field] = $result[0][strtolower($field)][0]; 179 } 180 } 181 182 // handle ActiveDirectory memberOf 183 $info['grps'] = $this->adldap->user_groups($user,(bool) $this->opts['recursive_groups']); 184 185 if (is_array($info['grps'])) { 186 foreach ($info['grps'] as $ndx => $group) { 187 $info['grps'][$ndx] = $this->cleanGroup($group); 188 } 189 } 190 191 // always add the default group to the list of groups 192 if(!is_array($info['grps']) || !in_array($conf['defaultgroup'],$info['grps'])){ 193 $info['grps'][] = $conf['defaultgroup']; 194 } 195 196 return $info; 197 } 198 199 /** 200 * Make AD group names usable by DokuWiki. 201 * 202 * Removes backslashes ('\'), pound signs ('#'), and converts spaces to underscores. 203 * 204 * @author James Van Lommel (jamesvl@gmail.com) 205 */ 206 function cleanGroup($name) { 207 $sName = str_replace('\\', '', $name); 208 $sName = str_replace('#', '', $sName); 209 $sName = preg_replace('[\s]', '_', $sName); 210 return $sName; 211 } 212 213 /** 214 * Sanitize user names 215 */ 216 function cleanUser($name) { 217 return $this->cleanGroup($name); 218 } 219 220 /** 221 * Most values in LDAP are case-insensitive 222 */ 223 function isCaseSensitive(){ 224 return false; 225 } 226 227 /** 228 * Bulk retrieval of user data 229 * 230 * @author Dominik Eckelmann <dokuwiki@cosmocode.de> 231 * @param start index of first user to be returned 232 * @param limit max number of users to be returned 233 * @param filter array of field/pattern pairs, null for no filter 234 * @return array of userinfo (refer getUserData for internal userinfo details) 235 */ 236 function retrieveUsers($start=0,$limit=-1,$filter=array()) { 237 if(!$this->_init()) return false; 238 239 if ($this->users === null) { 240 //get info for given user 241 $result = $this->adldap->all_users(); 242 if (!$result) return array(); 243 $this->users = array_fill_keys($result, false); 244 } 245 246 $i = 0; 247 $count = 0; 248 $this->_constructPattern($filter); 249 $result = array(); 250 251 foreach ($this->users as $user => &$info) { 252 if ($i++ < $start) { 253 continue; 254 } 255 if ($info === false) { 256 $info = $this->getUserData($user); 257 } 258 if ($this->_filter($user, $info)) { 259 $result[$user] = $info; 260 if (($limit >= 0) && (++$count >= $limit)) break; 261 } 262 } 263 return $result; 264 } 265 266 /** 267 * Modify user data 268 * 269 * @param $user nick of the user to be changed 270 * @param $changes array of field/value pairs to be changed 271 * @return bool 272 */ 273 function modifyUser($user, $changes) { 274 $return = true; 275 276 // password changing 277 if(isset($changes['pass'])){ 278 try { 279 $return = $this->adldap->user_password($user,$changes['pass']); 280 } catch (adLDAPException $e) { 281 if ($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1); 282 $return = false; 283 } 284 if(!$return) msg('AD Auth: failed to change the password. Maybe the password policy was not met?',-1); 285 } 286 287 // changing user data 288 $adchanges = array(); 289 if(isset($changes['name'])){ 290 // get first and last name 291 $parts = explode(' ',$changes['name']); 292 $adchanges['surname'] = array_pop($parts); 293 $adchanges['firstname'] = join(' ',$parts); 294 $adchanges['display_name'] = $changes['name']; 295 } 296 if(isset($changes['mail'])){ 297 $adchanges['email'] = $changes['mail']; 298 } 299 if(count($adchanges)){ 300 try { 301 $return = $return & $this->adldap->user_modify($user,$adchanges); 302 } catch (adLDAPException $e) { 303 if ($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1); 304 $return = false; 305 } 306 } 307 308 return $return; 309 } 310 311 /** 312 * Initialize the AdLDAP library and connect to the server 313 */ 314 function _init(){ 315 if(!is_null($this->adldap)) return true; 316 317 // connect 318 try { 319 $this->adldap = new adLDAP($this->opts); 320 if (isset($this->opts['ad_username']) && isset($this->opts['ad_password'])) { 321 $this->canDo['getUsers'] = true; 322 } 323 return true; 324 } catch (adLDAPException $e) { 325 if ($this->cnf['debug']) { 326 msg('AD Auth: '.$e->getMessage(), -1); 327 } 328 $this->success = false; 329 $this->adldap = null; 330 } 331 return false; 332 } 333 334 /** 335 * return 1 if $user + $info match $filter criteria, 0 otherwise 336 * 337 * @author Chris Smith <chris@jalakai.co.uk> 338 */ 339 function _filter($user, $info) { 340 foreach ($this->_pattern as $item => $pattern) { 341 if ($item == 'user') { 342 if (!preg_match($pattern, $user)) return 0; 343 } else if ($item == 'grps') { 344 if (!count(preg_grep($pattern, $info['grps']))) return 0; 345 } else { 346 if (!preg_match($pattern, $info[$item])) return 0; 347 } 348 } 349 return 1; 350 } 351 352 function _constructPattern($filter) { 353 $this->_pattern = array(); 354 foreach ($filter as $item => $pattern) { 355// $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/i'; // don't allow regex characters 356 $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/i'; // allow regex characters 357 } 358 } 359}