Table of Contents
The htaccess class
| UPDATED TO VERSION 1.2 — 5 June, 2006: See Changelog for new features and fixed bugs. |
|---|
In latest Dokuwiki, where wiki:auth:backends are php classes, the first wiki:tips:htaccessauth method doesn't work.
My solution is based on both the first revision of this page and the wiki:auth:plain authentication backend, and it's fully tested on 2006-03-09 DokuWiki version.
The wiki:acl feature is supported and User and Group Management is the same as in 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 htaccessauth.zip in your top dokuwiki folder or manually copy/paste the htaccess.class.php code. Follow instructions in htusers.auth.php and Configuration.
htaccess.class.php
Create a htaccess.class.php file in inc/auth/ directory with this contents:
<?php /** * Htaccess authentication backend * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @author Samuele Tognini <samuele@netsons.org> * Version: 1.2 * last modified: 2006-06-05 11:46:00 * * Work based on the plaintext authentication backend: * @author Andreas Gohr <andi@splitbrain.org> * @author Chris Smith <chris@jalakai.co.uk> * * and on the .htaccess authentication backed: * @author Marcel Meulemans <marcel_AT_meulemans_DOT_org> * Additions: Sebastian S <Seb.S@web.expr42.net> * */ 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 <samuele@netsons.org> */ 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 <andi@splitbrain.org> * Modified by Samuele Tognini <samuele@netsons.org> * @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 <samuele@netsons.org> * */ function logOff(){ global $conf; //works only with basic http authentication if (isset($conf['htaccess_realm'])) { $default_msg="Successful logout. Retry login <a href='".DOKU_BASE."'>here</a>."; 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 <andi@splitbrain.org> * Modified by Samuele Tognini <samuele@netsons.org> */ 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 <chris@jalakai.co.uk> * @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 <andi@splitbrain.org> * @author Chris Smith <chris@jalakai.co.uk> * Modified by Samuele Tognini <samuele@netsons.org> */ 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 <chris@jalakai.co.uk> * Modified by Samuele Tognini <samuele@netsons.org> * @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 <chris@jalakai.co.uk> */ 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 <chris@jalakai.co.uk> * @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 <samuele@netsons.org> */ 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 <samuele@netsons.org> */ 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 <chris@jalakai.co.uk> */ 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 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 1)
- mail: User mail. If empty and if it is configured the $conf['htacess_domain'] option in your configuration local.php, it'will be name@htacess_domain.2)
- 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 # <?php exit()?> # # 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 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 htusers.auth.php, whereas users that are already in htusers.auth.php will get automatically the htaccess_defaultgrp group.
Moreover, you can use acl to give custom permission to htaccess_defaultgrp.
The 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 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 htusers.auth.php.
Thanks to 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 htaccess_realm option. Only raw html code is supported.
Optional
default : Successful logout. Retry login <a href=dokuwiki_dir>login</a>.
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 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.phpare:
$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 htaccess_defaultgrp option as suggested by Dominique Launay.
- Fixed some little bugs.
- 2006-03-27 version 1.1:
- 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 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 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 3)
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 Tognini 2006-06-05 10:50
