summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndreas Gohr <gohr@cosmocode.de>2012-08-22 12:58:49 +0200
committerAndreas Gohr <gohr@cosmocode.de>2012-08-22 12:58:49 +0200
commit7b0a749accedb4a6e1f6fb0115d59707f586ec9f (patch)
tree133645f3e839e716c3c2b831c755148e8fec3d0e
parent5782941b257fd32e925d9a23b41846c6e17a7201 (diff)
downloadrpg-7b0a749accedb4a6e1f6fb0115d59707f586ec9f.tar.gz
rpg-7b0a749accedb4a6e1f6fb0115d59707f586ec9f.tar.bz2
Better support for multi domain AD setups
This changes how the AD auth backend handles multiple domains. It is now possible to configure multiple authentication domains even when not using SSO. USers can provide a domain in NTLM- and Kerberos-Style (prepended with a backslash, appended with a @-char). IMPORTANT: If you used AD auth before, you will need to adjust your ACLs and $conf['superuser'] settings. This patch changes how user names are cleaned. Spaces and other special chars are no longer removed. The only adjustment is lowercasing the username and streamlining the domain handling. User's login names will now contain the domain name in Kerberos style (user@yourdomain.com) when they logged in a non-default domain. You need to make sure your ACLs are setup accordingly. Domain names are always lowercased and need to be specified lowercased in the config.
-rw-r--r--inc/auth/ad.class.php320
1 files changed, 196 insertions, 124 deletions
diff --git a/inc/auth/ad.class.php b/inc/auth/ad.class.php
index e161c2939..48f3b9247 100644
--- a/inc/auth/ad.class.php
+++ b/inc/auth/ad.class.php
@@ -42,11 +42,12 @@
require_once(DOKU_INC.'inc/adLDAP.php');
class auth_ad extends auth_basic {
- var $cnf = null;
- var $opts = null;
- var $adldap = null;
+ var $cnf = array();
+ var $opts = array();
+ var $adldap = array();
var $users = null;
var $msgshown = false;
+ var $_pattern = array();
/**
* Constructor
@@ -56,58 +57,34 @@ class auth_ad extends auth_basic {
$this->cnf = $conf['auth']['ad'];
// additional information fields
- if (isset($this->cnf['additional'])) {
+ if(isset($this->cnf['additional'])) {
$this->cnf['additional'] = str_replace(' ', '', $this->cnf['additional']);
$this->cnf['additional'] = explode(',', $this->cnf['additional']);
} else $this->cnf['additional'] = array();
// ldap extension is needed
- if (!function_exists('ldap_connect')) {
- if ($this->cnf['debug'])
- msg("AD Auth: PHP LDAP extension not found.",-1);
+ if(!function_exists('ldap_connect')) {
+ if($this->cnf['debug'])
+ msg("AD Auth: PHP LDAP extension not found.", -1);
$this->success = false;
return;
}
// Prepare SSO
- if(!utf8_check($_SERVER['REMOTE_USER'])){
+ if(!utf8_check($_SERVER['REMOTE_USER'])) {
$_SERVER['REMOTE_USER'] = utf8_encode($_SERVER['REMOTE_USER']);
}
- if($_SERVER['REMOTE_USER'] && $this->cnf['sso']){
- // remove possible NTLM domain
- list($dom,$usr) = explode('\\',$_SERVER['REMOTE_USER'],2);
- if(!$usr) $usr = $dom;
-
- // remove possible Kerberos domain
- list($usr,$dom) = explode('@',$usr);
-
- $dom = strtolower($dom);
- $_SERVER['REMOTE_USER'] = $usr;
+ if($_SERVER['REMOTE_USER'] && $this->cnf['sso']) {
+ $_SERVER['REMOTE_USER'] = $this->cleanUser($_SERVER['REMOTE_USER']);
// we need to simulate a login
- if(empty($_COOKIE[DOKU_COOKIE])){
+ if(empty($_COOKIE[DOKU_COOKIE])) {
$_REQUEST['u'] = $_SERVER['REMOTE_USER'];
$_REQUEST['p'] = 'sso_only';
}
}
- // prepare adLDAP standard configuration
- $this->opts = $this->cnf;
-
- // add possible domain specific configuration
- if($dom && is_array($this->cnf[$dom])) foreach($this->cnf[$dom] as $key => $val){
- $this->opts[$key] = $val;
- }
-
- // handle multiple AD servers
- $this->opts['domain_controllers'] = explode(',',$this->opts['domain_controllers']);
- $this->opts['domain_controllers'] = array_map('trim',$this->opts['domain_controllers']);
- $this->opts['domain_controllers'] = array_filter($this->opts['domain_controllers']);
-
- // we can change the password if SSL is set
- if($this->opts['use_ssl'] || $this->opts['use_tls']){
- $this->cando['modPass'] = true;
- }
+ // other can do's are changed in $this->_loadServerConfig() base on domain setup
$this->cando['modName'] = true;
$this->cando['modMail'] = true;
}
@@ -120,15 +97,19 @@ class auth_ad extends auth_basic {
* to the LDAP server
*
* @author James Van Lommel <james@nosq.com>
+ * @param string $user
+ * @param string $pass
* @return bool
*/
- function checkPass($user, $pass){
+ function checkPass($user, $pass) {
if($_SERVER['REMOTE_USER'] &&
- $_SERVER['REMOTE_USER'] == $user &&
- $this->cnf['sso']) return true;
+ $_SERVER['REMOTE_USER'] == $user &&
+ $this->cnf['sso']
+ ) return true;
- if(!$this->_init()) return false;
- return $this->adldap->authenticate($user, $pass);
+ $adldap = $this->_adldap($this->_userDomain($user));
+ if(!$adldap) return false;
+ return $adldap->authenticate($user, $pass);
}
/**
@@ -137,85 +118,92 @@ class auth_ad extends auth_basic {
* Returns info about the given user needs to contain
* at least these fields:
*
- * name string full name of the user
- * mail string email address of the user
- * grps array list of groups the user is in
+ * name string full name of the user
+ * mail string email address of the user
+ * grps array list of groups the user is in
*
- * This LDAP specific function returns the following
+ * This AD specific function returns the following
* addional fields:
*
- * dn string distinguished name (DN)
- * uid string Posix User ID
+ * dn string distinguished name (DN)
+ * uid string samaccountname
+ * lastpwd int timestamp of the date when the password was set
+ * expires true if the password expires
+ * expiresin int seconds until the password expires
+ * any fields specified in the 'additional' config option
*
* @author James Van Lommel <james@nosq.com>
+ * @param string $user
+ * @return array
*/
- function getUserData($user){
+ function getUserData($user) {
global $conf;
global $lang;
global $ID;
- if(!$this->_init()) return false;
+ $adldap = $this->_adldap($this->_userDomain($user));
+ if(!$adldap) return false;
if($user == '') return array();
- $fields = array('mail','displayname','samaccountname','lastpwd','pwdlastset','useraccountcontrol');
+ $fields = array('mail', 'displayname', 'samaccountname', 'lastpwd', 'pwdlastset', 'useraccountcontrol');
// add additional fields to read
$fields = array_merge($fields, $this->cnf['additional']);
$fields = array_unique($fields);
//get info for given user
- $result = $this->adldap->user_info($user, $fields);
- if($result == false){
+ $result = $adldap->user_info($user, $fields);
+ if($result == false) {
return array();
}
//general user info
- $info['name'] = $result[0]['displayname'][0];
- $info['mail'] = $result[0]['mail'][0];
- $info['uid'] = $result[0]['samaccountname'][0];
- $info['dn'] = $result[0]['dn'];
+ $info['name'] = $result[0]['displayname'][0];
+ $info['mail'] = $result[0]['mail'][0];
+ $info['uid'] = $result[0]['samaccountname'][0];
+ $info['dn'] = $result[0]['dn'];
//last password set (Windows counts from January 1st 1601)
$info['lastpwd'] = $result[0]['pwdlastset'][0] / 10000000 - 11644473600;
//will it expire?
$info['expires'] = !($result[0]['useraccountcontrol'][0] & 0x10000); //ADS_UF_DONT_EXPIRE_PASSWD
// additional information
- foreach ($this->cnf['additional'] as $field) {
- if (isset($result[0][strtolower($field)])) {
+ foreach($this->cnf['additional'] as $field) {
+ if(isset($result[0][strtolower($field)])) {
$info[$field] = $result[0][strtolower($field)][0];
}
}
// handle ActiveDirectory memberOf
- $info['grps'] = $this->adldap->user_groups($user,(bool) $this->opts['recursive_groups']);
+ $info['grps'] = $adldap->user_groups($user, (bool) $this->opts['recursive_groups']);
- if (is_array($info['grps'])) {
- foreach ($info['grps'] as $ndx => $group) {
+ if(is_array($info['grps'])) {
+ foreach($info['grps'] as $ndx => $group) {
$info['grps'][$ndx] = $this->cleanGroup($group);
}
}
// always add the default group to the list of groups
- if(!is_array($info['grps']) || !in_array($conf['defaultgroup'],$info['grps'])){
+ if(!is_array($info['grps']) || !in_array($conf['defaultgroup'], $info['grps'])) {
$info['grps'][] = $conf['defaultgroup'];
}
// check expiry time
- if($info['expires'] && $this->cnf['expirywarn']){
- $result = $this->adldap->domain_info(array('maxpwdage')); // maximum pass age
- $maxage = -1 * $result['maxpwdage'][0] / 10000000; // negative 100 nanosecs
- $timeleft = $maxage - (time() - $info['lastpwd']);
- $timeleft = round($timeleft/(24*60*60));
+ if($info['expires'] && $this->cnf['expirywarn']) {
+ $result = $adldap->domain_info(array('maxpwdage')); // maximum pass age
+ $maxage = -1 * $result['maxpwdage'][0] / 10000000; // negative 100 nanosecs
+ $timeleft = $maxage - (time() - $info['lastpwd']);
+ $timeleft = round($timeleft / (24 * 60 * 60));
$info['expiresin'] = $timeleft;
// if this is the current user, warn him (once per request only)
- if( ($_SERVER['REMOTE_USER'] == $user) &&
+ if(($_SERVER['REMOTE_USER'] == $user) &&
($timeleft <= $this->cnf['expirywarn']) &&
!$this->msgshown
- ){
- $msg = sprintf($lang['authpwdexpire'],$timeleft);
- if($this->canDo('modPass')){
- $url = wl($ID,array('do'=>'profile'));
+ ) {
+ $msg = sprintf($lang['authpwdexpire'], $timeleft);
+ if($this->canDo('modPass')) {
+ $url = wl($ID, array('do'=> 'profile'));
$msg .= ' <a href="'.$url.'">'.$lang['btn_profile'].'</a>';
}
msg($msg);
@@ -242,15 +230,38 @@ class auth_ad extends auth_basic {
/**
* Sanitize user names
+ *
+ * Normalizes domain parts, does not modify the user name itself (unlike cleanGroup)
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ * @param string $name
+ * @return string
*/
function cleanUser($name) {
- return $this->cleanGroup($name);
+ // get NTLM or Kerberos domain part
+ list($dom, $name) = explode('\\', $name, 2);
+ if(!$name) $name = $dom;
+ list($name, $dom) = explode('@', $name, 2);
+
+ // clean up both
+ $dom = strtolower(trim($dom));
+ $name = strtolower(trim($name));
+
+ // is this a known, valid domain? if not discard
+ if(!is_array($this->cnf[$dom])) {
+ $dom = '';
+ }
+
+ // reattach domain
+ if($dom) $name = "$name@$dom";
+
+ return $name;
}
/**
* Most values in LDAP are case-insensitive
*/
- function isCaseSensitive(){
+ function isCaseSensitive() {
return false;
}
@@ -258,36 +269,37 @@ class auth_ad extends auth_basic {
* Bulk retrieval of user data
*
* @author Dominik Eckelmann <dokuwiki@cosmocode.de>
- * @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, null for no filter
- * @return array of userinfo (refer getUserData for internal userinfo details)
+ * @param int $start index of first user to be returned
+ * @param int $limit max number of users to be returned
+ * @param array $filter array of field/pattern pairs, null for no filter
+ * @return array userinfo (refer getUserData for internal userinfo details)
*/
- function retrieveUsers($start=0,$limit=-1,$filter=array()) {
- if(!$this->_init()) return false;
+ function retrieveUsers($start = 0, $limit = -1, $filter = array()) {
+ $adldap = $this->_adldap(null);
+ if(!$adldap) return false;
- if ($this->users === null) {
+ if($this->users === null) {
//get info for given user
- $result = $this->adldap->all_users();
- if (!$result) return array();
+ $result = $adldap->all_users();
+ if(!$result) return array();
$this->users = array_fill_keys($result, false);
}
- $i = 0;
+ $i = 0;
$count = 0;
$this->_constructPattern($filter);
$result = array();
- foreach ($this->users as $user => &$info) {
- if ($i++ < $start) {
+ foreach($this->users as $user => &$info) {
+ if($i++ < $start) {
continue;
}
- if ($info === false) {
+ if($info === false) {
$info = $this->getUserData($user);
}
- if ($this->_filter($user, $info)) {
+ if($this->_filter($user, $info)) {
$result[$user] = $info;
- if (($limit >= 0) && (++$count >= $limit)) break;
+ if(($limit >= 0) && (++$count >= $limit)) break;
}
}
return $result;
@@ -296,41 +308,43 @@ class auth_ad extends auth_basic {
/**
* Modify user data
*
- * @param $user nick of the user to be changed
- * @param $changes array of field/value pairs to be changed
+ * @param string $user nick of the user to be changed
+ * @param array $changes array of field/value pairs to be changed
* @return bool
*/
function modifyUser($user, $changes) {
$return = true;
+ $adldap = $this->_adldap($this->_userDomain($user));
+ if(!$adldap) return false;
// password changing
- if(isset($changes['pass'])){
+ if(isset($changes['pass'])) {
try {
- $return = $this->adldap->user_password($user,$changes['pass']);
- } catch (adLDAPException $e) {
- if ($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
+ $return = $adldap->user_password($user, $changes['pass']);
+ } catch(adLDAPException $e) {
+ if($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
$return = false;
}
- if(!$return) msg('AD Auth: failed to change the password. Maybe the password policy was not met?',-1);
+ if(!$return) msg('AD Auth: failed to change the password. Maybe the password policy was not met?', -1);
}
// changing user data
$adchanges = array();
- if(isset($changes['name'])){
+ if(isset($changes['name'])) {
// get first and last name
- $parts = explode(' ',$changes['name']);
- $adchanges['surname'] = array_pop($parts);
- $adchanges['firstname'] = join(' ',$parts);
+ $parts = explode(' ', $changes['name']);
+ $adchanges['surname'] = array_pop($parts);
+ $adchanges['firstname'] = join(' ', $parts);
$adchanges['display_name'] = $changes['name'];
}
- if(isset($changes['mail'])){
+ if(isset($changes['mail'])) {
$adchanges['email'] = $changes['mail'];
}
- if(count($adchanges)){
+ if(count($adchanges)) {
try {
- $return = $return & $this->adldap->user_modify($user,$adchanges);
- } catch (adLDAPException $e) {
- if ($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
+ $return = $return & $adldap->user_modify($user, $adchanges);
+ } catch(adLDAPException $e) {
+ if($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
$return = false;
}
}
@@ -340,40 +354,98 @@ class auth_ad extends auth_basic {
/**
* Initialize the AdLDAP library and connect to the server
+ *
+ * When you pass null as domain, it will reuse any existing domain.
+ * Eg. the one of the logged in user. It falls back to the default
+ * domain if no current one is available.
+ *
+ * @param string|null $domain The AD domain to use
+ * @return adLDAP|bool true if a connection was established
*/
- function _init(){
- if(!is_null($this->adldap)) return true;
+ protected function _adldap($domain) {
+ if(is_null($domain) && is_array($this->opts)) {
+ $domain = $this->opts['domain'];
+ }
+
+ $this->opts = $this->_loadServerConfig((string) $domain);
+ if(isset($this->adldap[$domain])) return $this->adldap[$domain];
// connect
try {
- $this->adldap = new adLDAP($this->opts);
- if (isset($this->opts['ad_username']) && isset($this->opts['ad_password'])) {
- $this->canDo['getUsers'] = true;
- }
- return true;
- } catch (adLDAPException $e) {
- if ($this->cnf['debug']) {
+ $this->adldap[$domain] = new adLDAP($this->opts);
+ return $this->adldap[$domain];
+ } catch(adLDAPException $e) {
+ if($this->cnf['debug']) {
msg('AD Auth: '.$e->getMessage(), -1);
}
- $this->success = false;
- $this->adldap = null;
+ $this->success = false;
+ $this->adldap[$domain] = null;
}
return false;
}
/**
+ * Get the domain part from a user
+ *
+ * @param $user
+ * @return string
+ */
+ protected function _userDomain($user) {
+ list(, $domain) = explode('@', $user, 2);
+ return $domain;
+ }
+
+ /**
+ * Fetch the configuration for the given AD domain
+ *
+ * @param string $domain current AD domain
+ * @return array
+ */
+ protected function _loadServerConfig($domain) {
+ // prepare adLDAP standard configuration
+ $opts = $this->cnf;
+
+ $opts['domain'] = $domain;
+
+ // add possible domain specific configuration
+ if($domain && is_array($this->cnf[$domain])) foreach($this->cnf[$domain] as $key => $val) {
+ $opts[$key] = $val;
+ }
+
+ // handle multiple AD servers
+ $opts['domain_controllers'] = explode(',', $opts['domain_controllers']);
+ $opts['domain_controllers'] = array_map('trim', $opts['domain_controllers']);
+ $opts['domain_controllers'] = array_filter($opts['domain_controllers']);
+
+ // we can change the password if SSL is set
+ if($opts['use_ssl'] || $opts['use_tls']) {
+ $this->cando['modPass'] = true;
+ } else {
+ $this->cando['modPass'] = false;
+ }
+
+ if(isset($opts['ad_username']) && isset($opts['ad_password'])) {
+ $this->cando['getUsers'] = true;
+ } else {
+ $this->cando['getUsers'] = true;
+ }
+
+ return $opts;
+ }
+
+ /**
* return 1 if $user + $info match $filter criteria, 0 otherwise
*
* @author Chris Smith <chris@jalakai.co.uk>
*/
function _filter($user, $info) {
- 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;
+ 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;
+ if(!preg_match($pattern, $info[$item])) return 0;
}
}
return 1;
@@ -381,8 +453,8 @@ class auth_ad extends auth_basic {
function _constructPattern($filter) {
$this->_pattern = array();
- foreach ($filter as $item => $pattern) {
- $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/i'; // allow regex characters
+ foreach($filter as $item => $pattern) {
+ $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
}
}
}