====== The htaccess class ====== ^ **UPDATED TO VERSION 1.2 --- 5 June, 2006**:\\ See [[htaccessauth#Changelog]] for new features and fixed bugs.\\ \\ :!: //If you upgrade from version 1.0, you have also to upgrade the format of your [[htaccessauth#htusers.auth.php]] file.// ^ In latest Dokuwiki, where [[doku>wiki:auth:backends]] are php classes, the first [[doku>wiki:tips:htaccessauth]] method doesn't work. My solution is based on both the first revision of this page and the [[doku>wiki:auth:plain]] authentication backend, and it's fully tested on [[http://www.splitbrain.org/_media/projects/dokuwiki/dokuwiki-2006-03-09.tgz?id=projects%3Adokuwiki&cache=cache|2006-03-09 ]] DokuWiki version. The [[doku>wiki:acl]] feature is supported and User and Group Management is the same as in [[doku>wiki:auth:plain]] authentication backend, except the fact that less user informations are needed, because they can be retrieved directly from your Web Server. ===== Installation ===== Unpack the [[http://samuele.netsons.org/dokuwiki/media/htaccessauth.zip|htaccessauth.zip]] in your top dokuwiki folder or manually copy/paste the [[htaccessauth#htaccess.class.php|htaccess.class.php]] code. Follow instructions in [[htaccessauth#htusers.auth.php]] and [[htaccessauth#Configuration]]. ===== htaccess.class.php ===== Create a ''htaccess.class.php'' file in ''inc/auth/'' directory with this contents: * Version: 1.2 * last modified: 2006-06-05 11:46:00 * * Work based on the plaintext authentication backend: * @author Andreas Gohr * @author Chris Smith * * and on the .htaccess authentication backed: * @author Marcel Meulemans * Additions: Sebastian S * */ define('DOKU_AUTH', dirname(__FILE__)); require_once(DOKU_AUTH.'/basic.class.php'); define('AUTH_USERFILE',DOKU_CONF.'htusers.auth.php'); if(isset($_REQUEST['u'])) $_REQUEST['u'] = cleanID($_REQUEST['u']); if(isset($_REQUEST['acl_user'])) $_REQUEST['acl_user'] = cleanID($_REQUEST['acl_user']); class auth_htaccess extends auth_basic { var $users = null; var $_pattern = array(); /** * Constructor * * Carry out sanity checks to ensure the object is * able to operate. Set capabilities. * * @author Samuele Tognini */ function auth_htaccess() { global $conf; if (!@is_readable(AUTH_USERFILE)){ $this->success = false; }else{ if(@is_writable(AUTH_USERFILE)){ $this->cando['addUser'] = true; $this->cando['delUser'] = true; $this->cando['modName'] = true; $this->cando['modMail'] = true; $this->cando['modLogin'] = true; $this->cando['modGroups'] = true; } $this->cando['getGroups'] = true; $this->cando['getUsers'] = true; $this->cando['getUserCount'] = true; $this->cando['logoff'] = true; } } /** * Check user+password [required auth function] * * Checks if the given user exists and the given * plaintext password is correct * * @author Andreas Gohr * Modified by Samuele Tognini * @return bool */ function checkPass($user='',$pass=''){ global $conf; //To test. it should work on all enviroment, more than PHP_AUTH_USER. //change also the local.conf. //if (isset($_SERVER['REMOTE_USER'])) { if(isset($_SERVER['PHP_AUTH_USER']) and isset($_SERVER['PHP_AUTH_PW'])) { $userinfo = $this->getUserData($_SERVER['PHP_AUTH_USER']); if ($userinfo === false) return false; return true; }else{ return false; } } /** * Logoff user * * @author Samuele Tognini * */ function logOff(){ global $conf; //works only with basic http authentication if (isset($conf['htaccess_realm'])) { $default_msg="Successful logout. Retry login here."; header('WWW-Authenticate: Basic realm="'.$conf['htaccess_realm'].'"'); header('HTTP/1.0 401 Unauthorized'); (isset($conf['htaccess_logout'])) ? print($conf['htaccess_logout']) : print($default_msg); exit; } else { //Workaround for unauthorized users. (isset($conf['htaccess_unauthurl'])) ? $url=$conf['htaccess_unauthurl'] : $url=".."; //True logout requested if ($this->checkPass('','')) $url=DOKU_BASE; header('Location:'.$url); } } /** * Return user info * * Returns info about the given user needs to contain * at least these fields: * * name string full name of the user * mail string email addres of the user * grps array list of groups the user is in * * @author Andreas Gohr * Modified by Samuele Tognini */ function getUserData($user,$ht_defaultgrp=true){ global $conf; if($this->users === null) $this->_loadUserData(); //Every user gets the default group if (isset($conf['htaccess_defaultgrp']) && $ht_defaultgrp) { $grps=$conf['htaccess_defaultgrp']; //Also user not in auth file gets a valid dokuwiki login if (!isset($this->users[$user])) { $name=$this->_get_posixname($user); (isset($conf['htaccess_domain'])) ? $mail = $user."@".$conf['htaccess_domain'] : $mail=''; $grps=array($grps); $this->users[$user] = compact('name','mail','grps'); } //htaccess_defaultgroup in first position (empty($this->users[$user]['grps'][0])) ? $this->users[$user]['grps'][0] = $grps : array_unshift($this->users[$user]['grps'],$grps); } return isset($this->users[$user]) ? $this->users[$user] : false; } /** * Remove one or more users from the list of registered users * * @author Christopher Smith * @param array $users array of users to be deleted * @return int the number of users deleted */ function deleteUsers($users) { if (!is_array($users) || empty($users)) return 0; if ($this->users === null) $this->_loadUserData(); $deleted = array(); foreach ($users as $user) { if (isset($this->users[$user])) $deleted[] = preg_quote($user,'/'); } if (empty($deleted)) return 0; $pattern = '/^('.join('|',$deleted).'):/'; if (io_deleteFromFile(AUTH_USERFILE,$pattern,true)) { foreach ($deleted as $user) unset($this->users[$user]); return count($deleted); } // problem deleting, reload the user list and count the difference $count = count($this->users()); $this->_loadUserData(); $count -= $count($this->users()); return $count; } /** * Create a new User * * Returns false if the user already exists, null when an error * occured and true if everything went well. * * The new user will be added to the default group by this * function if grps are not specified (default behaviour). * * @author Andreas Gohr * @author Chris Smith * Modified by Samuele Tognini */ function createUser($user,$pwd,$name,$mail,$grps=null){ global $conf; // user mustn't already exist in auth file if ($this->getUserData($user,false) !== false) { msg("User $user already exists.",-1); return false; } // set default group if no groups specified if (!is_array($grps)) $grps = array($conf['defaultgroup']); // prepare user line if ($mail == $user."@".$conf['htaccess_domain']) $mail = ''; $groups = join(',',$grps); $userline = join(':',array($user,$name,$mail,$groups))."\n"; if (io_saveFile(AUTH_USERFILE,$userline,true)) { //only for first visualization $name=$this->_get_posixname($user); $this->users[$user] = compact('name','mail','grps'); if (isset($conf['htaccess_defaultgrp'])) msg("User inherits the default htaccess group: ".$conf['htaccess_defaultgrp'],0); return true; } msg('The '.AUTH_USERFILE.' file is not writable. Please inform the Wiki-Admin',-1); return null; } /** * Modify user data * * @author Chris Smith * Modified by Samuele Tognini * @param $user nick of the user to be changed * @param $changes array of field/value pairs to be changed (password will be clear text) * @return bool */ function modifyUser($user, $changes) { global $conf; global $ACT; global $INFO; // sanity checks, user must already exist in auth file and there must be something to change if (($userinfo = $this->getUserData($user,false)) === false) return false; if (!is_array($changes) || !count($changes)) return true; // update userinfo with new data $newuser = $user; foreach ($changes as $field => $value) { if ($field == 'user') { $newuser = $value; continue; } $userinfo[$field] = $value; } //htaccess_defaultgrp first occurrence isn't added $a=array_search($conf['htaccess_defaultgrp'],$userinfo['grps']); if (is_numeric($a)) unset($userinfo['grps'][$a]); $groups = join(',',$userinfo['grps']); $newname= $userinfo['name']; $newmail=$userinfo['mail']; //User description is the same of system user. Write a blank field. if ($newname==$this->_get_posixname($newuser)) $newname=''; //Standard mail.Write a blank field. if (isset($conf['htaccess_domain']) && $newmail==$newuser."@".$conf['htaccess_domain']) $newmail=''; $userline = join(':',array($newuser, $newname, $newmail, $groups))."\n"; if (!$this->deleteUsers(array($user))) { msg('Unable to modify user data. Please inform the Wiki-Admin',-1); return false; } if (!io_saveFile(AUTH_USERFILE,$userline,true)) { msg('There was an error modifying your user data. You should register again.',-1); // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page $ACT == 'register'; return false; } $this->users[$newuser] = $userinfo; if (isset($conf['htaccess_defaultgrp'])) msg("User inherits the default htaccess group: ".$conf['htaccess_defaultgrp'],1); return true; } /** * Return a count of the number of user which meet $filter criteria * * @author Chris Smith */ function getUserCount($filter=array()) { if($this->users === null) $this->_loadUserData(); if (!count($filter)) return count($this->users); $count = 0; $this->_constructPattern($filter); foreach ($this->users as $user => $info) { $count += $this->_filter($user, $info); } return $count; } /** * Bulk retrieval of user data * * @author Chris Smith * @param start index of first user to be returned * @param limit max number of users to be returned * @param filter array of field/pattern pairs * @return array of userinfo (refer getUserData for internal userinfo details) */ function retrieveUsers($start=0,$limit=0,$filter=array()) { global $conf; $grps=$conf['htaccess_defaultgrp']; if ($this->users === null || !empty($grps)) $this->_loadUserData(); ksort($this->users); $i = 0; $count = 0; $out = array(); $this->_constructPattern($filter); foreach ($this->users as $user => $info) { //display the htaccess default group if (!empty($grps)) (empty($info['grps'][0])) ? $info['grps'][0] = $grps : array_unshift($info['grps'],$grps); if ($this->_filter($user, $info)) { if ($i >= $start) { $out[$user] = $info; $count++; if (($limit > 0) && ($count >= $limit)) break; } $i++; } } return $out; } /** * Return the user name information from system * * @author Samuele Tognini */ function _get_posixname ($sys_user){ $sys_name=''; $sys_user= posix_getpwnam($sys_user); if (@$sys_user) { list($id, $pass, $uid, $gid, $extra) = array_values($sys_user); list($username, $recycle) = split(",", $extra, 2); $sys_name = urldecode($username); } return $sys_name; } /** * Load all user data * * loads the user file into a datastructure * * @author Samuele Tognini */ function _loadUserData(){ global $conf; $this->users = array(); if(!@file_exists(AUTH_USERFILE)) return; $lines = file(AUTH_USERFILE); foreach($lines as $line){ $line = preg_replace('/#.*$/','',$line); //ignore comments $line = trim($line); if(empty($line)) continue; $row = split(":",$line,4); if (isset($row[3])) { $tmp_name = $row[1]; $tmp_mail = $row[2]; //i try to get some user info from system if (empty($tmp_name)) { if ($sys_user= posix_getpwnam($row[0])) { list($id, $pass, $uid, $gid, $extra) = array_values($sys_user); list($username, $recycle) = split(",", $extra, 2); $tmp_name = $username; } } if (empty($tmp_mail) && isset($conf['htaccess_domain'])) $tmp_mail = $row[0]."@".$conf['htaccess_domain']; $user_groups=$row[3]; $groups = split(",",$user_groups); $this->users[$row[0]]['name'] = urldecode($tmp_name); $this->users[$row[0]]['mail'] = $tmp_mail; $this->users[$row[0]]['grps'] = $groups; } else { msg("Errors for user ".$row[0].": Check your ".AUTH_USERFILE,-1); $this->users[$row[0]] = ''; } } } /** * return 1 if $user + $info match $filter criteria, 0 otherwise * * @author Chris Smith */ function _filter($user, $info) { // FIXME foreach ($this->_pattern as $item => $pattern) { if ($item == 'user') { if (!preg_match($pattern, $user)) return 0; } else if ($item == 'grps') { if (!count(preg_grep($pattern, $info['grps']))) return 0; } else { if (!preg_match($pattern, $info[$item])) return 0; } } return 1; } function _constructPattern($filter) { $this->_pattern = array(); foreach ($filter as $item => $pattern) { // $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/'; // don't allow regex characters $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/'; // allow regex characters } } } //Setup VIM: ex: et ts=2 enc=utf-8 : ===== htusers.auth.php ===== Create a ''htusers.auth.php'' file in ''conf/'' directory and write here your list of DokuWiki users, their groups and their optional full names and mail. If you want use the [[doku>plugin:user_manager|User Manager]] plugin in order to manage users through web, give write permissions on file to web user: #> chgrp www-data conf/htusers.auth.php #> chmod 664 conf/htusers.auth.php The file format is ''name:full_name:mail:groups'': * **name**: Login Name. It must be also in the .htaccess (Basic HTTP Authorization) file. * **full name**: User description. If empty, htaccessauth will try to get it from system ((Optional)) * **mail**: User mail. If empty and if it is configured the $conf['htacess_domain'] option in your [[htaccessauth#configuration]] local.php, it'will be name@htacess_domain.((Optional)) * **groups**: User Groups. Comma separated user groups. **Remeber:** these users must have also Basic HTTP Authorization in .htaccess file or access will be denied anyway. This is an example: # htusers.auth.php # # # Htaccess Userfile # # Format: # # user:full name:mail:groups,comma,separated jack:The Admin::admins allen:::editors,uploaders neal::allen@hisdomain.net:editors ===== acl.auth.php ===== Copy the standard distribution file ''conf/acl.auth.php.dist'' to ''conf/acl.auth.php'', and give write permission to Web service user if you want change ACL through web interface. For example: #> chgrp www-data conf/acl.auth.php #> chmod 664 conf/acl.auth.php ===== Configuration ===== In order to use htaccessauth you must use these settings in your ''conf/local.php'': $conf['authtype'] = 'htaccess'; if (isset($_SERVER['PHP_AUTH_USER']) and !isset($_SESSION[$conf['title']]['auth']['info'])) $_REQUEST['u'] = $_SERVER['PHP_AUTH_USER']; ==== Optional settings ==== These settings in ''conf/local.php'' make customizable the htaccess backend: === htaccess_defaultgrp === Don't confuse it with [[doku>wiki:config#defaultgroup]] option. Every user which get Basic HTTP authorization, gains automatically the ''htaccess_defaultgrp'' dokuwiki group too. In this way you don't need anymore to create users in [[htaccessauth#htusers.auth.php]], whereas users that are already in [[htaccessauth#htusers.auth.php]] will get automatically the ''htaccess_defaultgrp'' group. Moreover, you can use acl to give custom permission to ''htaccess_defaultgrp''. The [[doku>plugin:user_manager|User Manager]] plugin works correctly (when you create,modify or display users, it automatically give them the 'htaccess_defaultgrp' group), but it doesn't display the users that aren't in htaccess.auth.php. Note that ''htaccess_defaultgrp'' will not be really writed by user manager inside [[htaccessauth#htusers.auth.php]], so that, if later you unset the $conf['htaccess_defaultgrp'] config, they will lost the ''htaccess_defaultgrp'' group and also the dokuwiki access if they aren't in [[htaccessauth#htusers.auth.php]]. Thanks to [[dominique.launay@cru.fr|Dominique Launay]] for this great idea. __Optional__ \\ __default__ : not used \\ __Example__ : $conf['"htaccess_defaultgrp"] = "guest"; === htaccess_domain === You can either set a custom mail for every user in htusers.auth.php mail field, or leave it empty and set the htaccess_domain, so that users mail becames automatically user@htaccess_domain. __Optional__ \\ __default__ : not used \\ __Example__ : $conf['htaccess_domain'] = "example.com"; === htaccess_realm === The basic http authentication realm. This is the AuthName directive in Apache web server configuration. You can find its value also in the login popup dialog. If it's set then users will be able to logoff from Basic Http authentication using the logout button, if not then logout button will redirect users to the wiki main page. A wrong value makes the logoff unsuccessful. __Optional__ \\ __default__ : not used \\ __Example__ : $conf["htaccess_realm"]="Limited Site" **Note:** In order to logoff, click on dokuwiki 'logout' button, then click on then 'cancel' button of the login popup dialog. === htaccess_logout === You can display a custom message to users that logout dokuwiki through the [[htaccessauth#htaccess_realm]] option. Only raw html code is supported. __Optional__ \\ __default__ : Successful logout. Retry login login. \\ __Example__ : $conf["htaccess_logout"]="This is my message to you." === htaccess_unauthurl === This is a simple workaround to use when dokuwiki acl system denies access to users, that has authenticated themself successfully through the HTTP Basic authentication system. In this case, when htaccess_unauthurl value is also a denied page,a loop can be generated ,so pay attention to set it correctly. Unuseful togheter with the [[htaccessauth#htaccess_realm]] option. __Optional__ \\ __default__ : .. (the directory upper dokuwiki, change it if it's a denied path) \\ __Example__ : $conf["htaccess_unauthurl"]="http://www.mysite.com" ==== Others recommended options ==== Others good options in ''conf/local.php''are: $conf['useacl'] = 1; // this enables the ACL feature $conf['openregister'] = 0; // Since you are using Basic HTTP Registration, i think openregister will not work . $conf['superuser'] = '@admin'; // admin group is superuser, choose a user or a group from you htuser.auth.file ==== local.php configuration example ==== $conf['authtype'] = 'htaccess'; $conf['useacl'] = 1; $conf['openregister'] = 0; $conf['superuser'] = '@admin'; $conf['htaccess_defaultgrp'] = "guest"; $conf['htaccess_domain'] = "example.com"; if (isset($_SERVER['PHP_AUTH_USER']) and !isset($_SESSION[$conf['title']]['auth']['info'])) $_REQUEST['u'] = $_SERVER['PHP_AUTH_USER']; ===== Changelog ===== * **2006-06-05 version 1.2**: * Added the [[htaccessauth#htaccess_defaultgrp]] option as suggested by [[dominique.launay@cru.fr|Dominique Launay]]. * Fixed some little bugs. * **2006-03-27 version 1.1**: * [[doku>plugin:user_manager|User manager]] plugin is fully compatible: now you can create,modify and delete users through web inteface. * New logoff feature: allow an Http Basic Htaccess logout or make unuseful the logout button. * New custom mail option: set a custom mail or use the global domain variable. :!: Upgrade your [[htaccessauth#htusers.auth.php]] to avoid errors. * New custom full name option: set a custom user full name or let htaccessauth retrive it from system. :!: Upgrade your [[htaccessauth#htusers.auth.php]] to avoid errors. * Fixed the 'empty array' bug error. * **2006-03-16 version 1.0**: * First release ====== Notes ====== The module as provided only works if you are using mod_php. It works fine without mod_php if you make the following changes: if (isset($_SERVER['REMOTE_USER']) and !isset($_SESSION[$conf['title']]['auth']['info'])) $_REQUEST['u'] = $_SERVER['REMOTE_USER']; And in the module you need this extra block at line 77 ((At else statment of checkpass function)) elseif( isset($_SERVER['REMOTE_USER']) ) { $userinfo = $this->getUserData($_SERVER['REMOTE_USER']); if ($userinfo === false) return false; return true; } >Reading documentation, it seems that $_SERVER['REMOTE_USER'] can be used in place of $_SERVER['PHP_AUTH_USER'] even with php_mode. For now, i don't change code because it needs testing feedback. --- //[[samuele@netsons.org|Samuele Tognini]] 2006-06-05 10:50//