404 lines
15 KiB
PHP
404 lines
15 KiB
PHP
|
<?php
|
||
|
|
||
|
/**
|
||
|
* Tiny Tiny RSS plugin for LDAP authentication
|
||
|
* @author tsmgeek (tsmgeek@gmail.com)
|
||
|
* @author hydrian (ben.tyger@tygerclan.net)
|
||
|
* @copyright GPL2
|
||
|
* Requires php-ldap
|
||
|
* @version 2.00
|
||
|
*/
|
||
|
/**
|
||
|
* Configuration
|
||
|
* Put the following options in config.php and customize them for your environment
|
||
|
*
|
||
|
* define('LDAP_AUTH_SERVER_URI', 'ldaps://LDAPServerHostname:port/');
|
||
|
* define('LDAP_AUTH_USETLS', FALSE); // Enable TLS Support for ldaps://
|
||
|
* define('LDAP_AUTH_ALLOW_UNTRUSTED_CERT', TRUE); // Allows untrusted certificate
|
||
|
* define('LDAP_AUTH_BASEDN', 'dc=example,dc=com');
|
||
|
* define('LDAP_AUTH_ANONYMOUSBEFOREBIND', FALSE);
|
||
|
* // ??? will be replaced with the entered username(escaped) at login
|
||
|
* define('LDAP_AUTH_SEARCHFILTER', '(&(objectClass=person)(uid=???))');
|
||
|
* // Optional configuration
|
||
|
* define('LDAP_AUTH_BINDDN', 'cn=serviceaccount,dc=example,dc=com');
|
||
|
* define('LDAP_AUTH_BINDPW', 'ServiceAccountsPassword');
|
||
|
* define('LDAP_AUTH_LOGIN_ATTRIB', 'uid');
|
||
|
* define('LDAP_AUTH_LOG_ATTEMPTS', FALSE);
|
||
|
* Enable Debug Logging
|
||
|
* define('LDAP_AUTH_DEBUG', FALSE);
|
||
|
*
|
||
|
*
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Notes -
|
||
|
* LDAP search does not support follow ldap referals. Referals are disabled to
|
||
|
* allow proper login. This is particular to Active Directory.
|
||
|
*
|
||
|
* Also group membership can be supported if the user object contains the
|
||
|
* the group membership via attributes. The following LDAP servers can
|
||
|
* support this.
|
||
|
* * Active Directory
|
||
|
* * OpenLDAP support with MemberOf Overlay
|
||
|
*
|
||
|
*/
|
||
|
class Auth_Ldap extends Plugin implements IAuthModule {
|
||
|
|
||
|
private $link;
|
||
|
private $host;
|
||
|
private $base;
|
||
|
private $logClass;
|
||
|
private $ldapObj = NULL;
|
||
|
private $_debugMode;
|
||
|
private $_serviceBindDN;
|
||
|
private $_serviceBindPass;
|
||
|
private $_baseDN;
|
||
|
private $_useTLS;
|
||
|
private $_host;
|
||
|
private $_port;
|
||
|
private $_scheme;
|
||
|
private $_schemaCacheEnabled;
|
||
|
private $_anonBeforeBind;
|
||
|
private $_allowUntrustedCerts;
|
||
|
private $_ldapLoginAttrib;
|
||
|
|
||
|
function about() {
|
||
|
return array(0.05,
|
||
|
"Authenticates against an LDAP server (configured in config.php)",
|
||
|
"hydrian",
|
||
|
true);
|
||
|
}
|
||
|
|
||
|
function init($host) {
|
||
|
$this->link = $host->get_link();
|
||
|
$this->host = $host;
|
||
|
$this->base = new Auth_Base($this->link);
|
||
|
|
||
|
$host->add_hook($host::HOOK_AUTH_USER, $this);
|
||
|
}
|
||
|
|
||
|
private function _log($msg, $level = E_USER_NOTICE, $file = '', $line = 0, $context = '') {
|
||
|
$loggerFunction = Logger::get();
|
||
|
if (is_object($loggerFunction)) {
|
||
|
$loggerFunction->log_error($level, $msg, $file, $line, $context);
|
||
|
} else {
|
||
|
trigger_error($msg, $level);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Logs login attempts
|
||
|
* @param string $username Given username that attempts to log in to TTRSS
|
||
|
* @param string $result "Logging message for type of result. (Success / Fail)"
|
||
|
* @return boolean
|
||
|
* @deprecated
|
||
|
*
|
||
|
* Now that _log support syslog and log levels and graceful fallback user.
|
||
|
*/
|
||
|
private function _logAttempt($username, $result) {
|
||
|
|
||
|
|
||
|
return trigger_error('TT-RSS Login Attempt: user ' . (string) $username .
|
||
|
' attempted to login (' . (string) $result . ') from ' . (string) $ip, E_USER_NOTICE
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $subject The subject string
|
||
|
* @param string $ignore Set of characters to leave untouched
|
||
|
* @param int $flags Any combination of LDAP_ESCAPE_* flags to indicate the
|
||
|
* set(s) of characters to escape.
|
||
|
* @return string
|
||
|
**/
|
||
|
function ldap_escape($subject, $ignore = '', $flags = 0)
|
||
|
{
|
||
|
if (!function_exists('ldap_escape')) {
|
||
|
define('LDAP_ESCAPE_FILTER', 0x01);
|
||
|
define('LDAP_ESCAPE_DN', 0x02);
|
||
|
|
||
|
static $charMaps = array(
|
||
|
LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"),
|
||
|
LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'),
|
||
|
);
|
||
|
|
||
|
// Pre-process the char maps on first call
|
||
|
if (!isset($charMaps[0])) {
|
||
|
$charMaps[0] = array();
|
||
|
for ($i = 0; $i < 256; $i++) {
|
||
|
$charMaps[0][chr($i)] = sprintf('\\%02x', $i);;
|
||
|
}
|
||
|
|
||
|
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_FILTER]); $i < $l; $i++) {
|
||
|
$chr = $charMaps[LDAP_ESCAPE_FILTER][$i];
|
||
|
unset($charMaps[LDAP_ESCAPE_FILTER][$i]);
|
||
|
$charMaps[LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr];
|
||
|
}
|
||
|
|
||
|
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_DN]); $i < $l; $i++) {
|
||
|
$chr = $charMaps[LDAP_ESCAPE_DN][$i];
|
||
|
unset($charMaps[LDAP_ESCAPE_DN][$i]);
|
||
|
$charMaps[LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create the base char map to escape
|
||
|
$flags = (int)$flags;
|
||
|
$charMap = array();
|
||
|
if ($flags & LDAP_ESCAPE_FILTER) {
|
||
|
$charMap += $charMaps[LDAP_ESCAPE_FILTER];
|
||
|
}
|
||
|
if ($flags & LDAP_ESCAPE_DN) {
|
||
|
$charMap += $charMaps[LDAP_ESCAPE_DN];
|
||
|
}
|
||
|
if (!$charMap) {
|
||
|
$charMap = $charMaps[0];
|
||
|
}
|
||
|
|
||
|
// Remove any chars to ignore from the list
|
||
|
$ignore = (string)$ignore;
|
||
|
for ($i = 0, $l = strlen($ignore); $i < $l; $i++) {
|
||
|
unset($charMap[$ignore[$i]]);
|
||
|
}
|
||
|
|
||
|
// Do the main replacement
|
||
|
$result = strtr($subject, $charMap);
|
||
|
|
||
|
// Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed
|
||
|
if ($flags & LDAP_ESCAPE_DN) {
|
||
|
if ($result[0] === ' ') {
|
||
|
$result = '\\20' . substr($result, 1);
|
||
|
}
|
||
|
if ($result[strlen($result) - 1] === ' ') {
|
||
|
$result = substr($result, 0, -1) . '\\20';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}else{
|
||
|
return ldap_escape($subject, $ignore, $flags);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Finds client's IP address
|
||
|
* @return string
|
||
|
*/
|
||
|
private function _getClientIP() {
|
||
|
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||
|
//check ip from share internet
|
||
|
|
||
|
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||
|
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||
|
//to check ip is pass from proxy
|
||
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||
|
} else {
|
||
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||
|
}
|
||
|
|
||
|
return $ip;
|
||
|
}
|
||
|
|
||
|
private function _getBindDNWord() {
|
||
|
return (strlen($this->_serviceBindDN) > 0 ) ? $this->_serviceBindDN : 'anonymous DN';
|
||
|
}
|
||
|
|
||
|
private function _getTempDir() {
|
||
|
if (!sys_get_temp_dir()) {
|
||
|
$tmpFile = tempnam();
|
||
|
$tmpDir = dirname($tmpFile);
|
||
|
unlink($tmpFile);
|
||
|
unset($tmpFile);
|
||
|
return $tmpDir;
|
||
|
} else {
|
||
|
return sys_get_temp_dir();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Main Authentication method
|
||
|
* Required for plugin interface
|
||
|
* @param unknown $login User's username
|
||
|
* @param unknown $password User's password
|
||
|
* @return boolean
|
||
|
*/
|
||
|
function authenticate($login, $password) {
|
||
|
if ($login && $password) {
|
||
|
|
||
|
if (!function_exists('ldap_connect')) {
|
||
|
trigger_error('auth_ldap requires PHP\'s PECL LDAP package installed.');
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
//Loading configuration
|
||
|
$this->_debugMode = defined('LDAP_AUTH_DEBUG') ?
|
||
|
LDAP_AUTH_DEBUG : FALSE;
|
||
|
|
||
|
$this->_anonBeforeBind = defined('LDAP_AUTH_ANONYMOUSBEFOREBIND') ?
|
||
|
LDAP_AUTH_ANONYMOUSBEFOREBIND : FALSE;
|
||
|
|
||
|
$this->_serviceBindDN = defined('LDAP_AUTH_BINDDN') ? LDAP_AUTH_BINDDN : null;
|
||
|
$this->_serviceBindPass = defined('LDAP_AUTH_BINDPW') ? LDAP_AUTH_BINDPW : null;
|
||
|
$this->_baseDN = defined('LDAP_AUTH_BASEDN') ? LDAP_AUTH_BASEDN : null;
|
||
|
if (!defined('LDAP_AUTH_BASEDN')) {
|
||
|
$this->_log('LDAP_AUTH_BASEDN is required and not defined.', E_USER_ERROR);
|
||
|
return FALSE;
|
||
|
} else {
|
||
|
$this->_baseDN = LDAP_AUTH_BASEDN;
|
||
|
}
|
||
|
|
||
|
$parsedURI = parse_url(LDAP_AUTH_SERVER_URI);
|
||
|
if ($parsedURI === FALSE) {
|
||
|
$this->_log('Could not parse LDAP_AUTH_SERVER_URI in config.php', E_USER_ERROR);
|
||
|
return FALSE;
|
||
|
}
|
||
|
$this->_host = $parsedURI['host'];
|
||
|
$this->_scheme = $parsedURI['scheme'];
|
||
|
|
||
|
if (is_int($parsedURI['port'])) {
|
||
|
$this->_port = $parsedURI['port'];
|
||
|
} else {
|
||
|
$this->_port = ($this->_scheme === 'ldaps') ? 636 : 389;
|
||
|
}
|
||
|
|
||
|
$this->_useTLS = defined('LDAP_AUTH_USETLS') ? LDAP_AUTH_USETLS : FALSE;
|
||
|
|
||
|
$this->_logAttempts = defined('LDAP_AUTH_LOG_ATTEMPTS') ?
|
||
|
LDAP_AUTH_LOG_ATTEMPTS : FALSE;
|
||
|
|
||
|
$this->_ldapLoginAttrib = defined('LDAP_AUTH_LOGIN_ATTRIB') ?
|
||
|
LDAP_AUTH_LOGIN_ATTRIB : null;
|
||
|
|
||
|
|
||
|
/**
|
||
|
Building LDAP connection
|
||
|
* */
|
||
|
$ldapConnParams = array(
|
||
|
'host' => $this->_host,
|
||
|
'basedn' => $this->_baseDN,
|
||
|
'port' => $this->_port,
|
||
|
'starttls' => $this->_useTLS
|
||
|
);
|
||
|
|
||
|
if ($this->_debugMode)
|
||
|
$this->_log(print_r($ldapConnParams, TRUE), E_USER_NOTICE);
|
||
|
$ldapConn = @ldap_connect($this->_host, $this->_port);
|
||
|
if ($ldapConn === FALSE) {
|
||
|
$this->_log('Could not connect to LDAP Server: \'' . $this->_host . '\'', E_USER_ERROR);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/* Enable LDAP protocol version 3. */
|
||
|
if (!@ldap_set_option($ldapConn, LDAP_OPT_PROTOCOL_VERSION, 3)) {
|
||
|
$this->_log('Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3', E_USER_ERROR);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/* Set referral option */
|
||
|
if (!@ldap_set_option($ldapConn, LDAP_OPT_REFERRALS, FALSE)) {
|
||
|
$this->_log('Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to TRUE', E_USER_ERROR);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (stripos($this->_host, "ldaps:") === FALSE and $this->_useTLS) {
|
||
|
if (!@ldap_start_tls($ldapConn)) {
|
||
|
$this->_log('Unable to force TLS', E_USER_ERROR);
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
$error = @ldap_bind($ldapConn, $this->_serviceBindDN, $this->_serviceBindPass);
|
||
|
if ($error === FALSE) {
|
||
|
$this->_log(
|
||
|
'LDAP bind(): Bind failed (' . $error . ')with DN ' . $this->_serviceBindDN, E_USER_ERROR
|
||
|
);
|
||
|
return FALSE;
|
||
|
} else {
|
||
|
$this->_log(
|
||
|
'Connected to LDAP Server: ' . LDAP_AUTH_SERVER_URI . ' with ' . $this->_getBindDNWord());
|
||
|
}
|
||
|
|
||
|
// Bind with service account if orignal connexion was anonymous
|
||
|
/* if (($this->_anonBeforeBind) && (strlen($this->_bindDN > 0))) {
|
||
|
$binding=$this->ldapObj->bind($this->_serviceBindDN, $this->_serviceBindPass);
|
||
|
if (get_class($binding) !== 'Net_LDAP2') {
|
||
|
$this->_log(
|
||
|
'Cound not bind service account: '.$binding->getMessage(),E_USER_ERROR);
|
||
|
return FALSE;
|
||
|
} else {
|
||
|
$this->_log('Bind with '.$this->_serviceBindDN.' successful.',E_USER_NOTICE);
|
||
|
}
|
||
|
} */
|
||
|
|
||
|
//Searching for user
|
||
|
$filterObj = str_replace('???', $this->ldap_escape($login), LDAP_AUTH_SEARCHFILTER);
|
||
|
$searchResults = @ldap_search($ldapConn, $this->_baseDN, $filterObj, array('displayName', 'title', 'sAMAccountName', $this->_ldapLoginAttrib), 0, 0, 0);
|
||
|
if ($searchResults === FALSE) {
|
||
|
$this->_log('LDAP Search Failed on base \'' . $this->_baseDN . '\' for \'' . $filterObj . '\'', E_USER_ERROR);
|
||
|
return FALSE;
|
||
|
}
|
||
|
$count = @ldap_count_entries($ldapConn, $searchResults);
|
||
|
if ($count === FALSE) {
|
||
|
|
||
|
} elseif ($count > 1) {
|
||
|
$this->_log('Multiple DNs found for username ' . (string) $login, E_USER_WARNING);
|
||
|
return FALSE;
|
||
|
} elseif ($count === 0) {
|
||
|
$this->_log((string) $login, 'Unknown User', E_USER_NOTICE);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
//Getting user's DN from search
|
||
|
$userEntry = @ldap_first_entry($ldapConn, $searchResults);
|
||
|
if ($userEntry === FALSE) {
|
||
|
$this->_log('LDAP search(): Unable to retrieve result after searching base \'' . $this->_baseDN . '\' for \'' . $filterObj . '\'', E_USER_WARNING);
|
||
|
return false;
|
||
|
}
|
||
|
$userAttributes = @ldap_get_attributes($ldapConn, $userEntry);
|
||
|
$userDN = @ldap_get_dn($ldapConn, $userEntry);
|
||
|
if ($userDN == FALSE) {
|
||
|
$this->_log('LDAP search(): Unable to get DN after searching base \'' . $this->_baseDN . '\' for \'' . $filterObj . '\'', E_USER_WARNING);
|
||
|
return false;
|
||
|
}
|
||
|
//Binding with user's DN.
|
||
|
if ($this->_debugMode)
|
||
|
$this->_log('Try to bind with user\'s DN: ' . $userDN);
|
||
|
$loginAttempt = @ldap_bind($ldapConn, $userDN, $password);
|
||
|
if ($loginAttempt === TRUE) {
|
||
|
$this->_log('User: ' . (string) $login . ' authentication successful');
|
||
|
if (strlen($this->_ldapLoginAttrib) > 0) {
|
||
|
if ($this->_debugMode)
|
||
|
$this->_log('Looking up TT-RSS username attribute in ' . $this->_ldapLoginAttrib);
|
||
|
$ttrssUsername = $userAttributes[$this->_ldapLoginAttrib][0];
|
||
|
;
|
||
|
@ldap_close($ldapConn);
|
||
|
if (!is_string($ttrssUsername)) {
|
||
|
$this->_log('Could not find user name attribute ' . $this->_ldapLoginAttrib . ' in LDAP entry', E_USER_WARNING);
|
||
|
return FALSE;
|
||
|
}
|
||
|
return $this->base->auto_create_user($ttrssUsername);
|
||
|
} else {
|
||
|
@ldap_close($ldapConn);
|
||
|
return $this->base->auto_create_user($login);
|
||
|
}
|
||
|
} else {
|
||
|
@ldap_close($ldapConn);
|
||
|
$this->_log('User: ' . (string) $login . ' authentication failed');
|
||
|
return FALSE;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns plugin API version
|
||
|
* Required for plugin interface
|
||
|
* @return number
|
||
|
*/
|
||
|
function api_version() {
|
||
|
return 2;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
?>
|