diff --git a/classes/Database/LDAP/Result.php b/classes/Database/LDAP/Result.php new file mode 100644 index 0000000..f7c54e6 --- /dev/null +++ b/classes/Database/LDAP/Result.php @@ -0,0 +1,4 @@ + diff --git a/classes/Kohana/Auth/LDAP.php b/classes/Kohana/Auth/LDAP.php index 3471c72..429a163 100644 --- a/classes/Kohana/Auth/LDAP.php +++ b/classes/Kohana/Auth/LDAP.php @@ -3,11 +3,10 @@ /** * LDAP Auth driver. * - * @package Kohana/LDAP - * @subpackage Auth/LDAP + * @package Auth/LDAP * @category Helpers * @author Deon George - * @copyright (c) phpLDAPadmin Development Team + * @copyright (c) 2013 phpLDAPadmin Development Team * @license http://dev.phpldapadmin.org/license.html */ class Kohana_Auth_LDAP extends Auth { @@ -38,7 +37,7 @@ class Kohana_Auth_LDAP extends Auth { // Load the user // @todo Get the server ID - $user = Database_LDAP::factory('user')->bind($username,$password); + $user = LDAP::factory('user')->bind($username,$password); } // @todo Implement conditional logging based on memberships to groups or other criteria. @@ -76,7 +75,7 @@ class Kohana_Auth_LDAP extends Auth { } public function logout($destroy=FALSE,$logout_all=FALSE) { - Database_LDAP::factory('user')->disconnect(); + LDAP::factory('user')->disconnect(); if (PHP_SAPI !== 'cli') return parent::logout($destroy,$logout_all); diff --git a/classes/Kohana/Database/LDAP.php b/classes/Kohana/Database/LDAP.php index 7ba8671..39a7255 100644 --- a/classes/Kohana/Database/LDAP.php +++ b/classes/Kohana/Database/LDAP.php @@ -6,42 +6,61 @@ * @package Kohana/LDAP * @category Database/LDAP * @author Deon George - * @copyright (c) phpLDAPadmin Development Team + * @copyright (c) 2013 phpLDAPadmin Development Team * @license http://dev.phpldapadmin.org/license.html */ -abstract class Kohana_Database_LDAP extends Database { - // Our required abstract functions - public function set_charset($charset) {} - public function query($type, $sql, $as_object = FALSE, array $params = NULL) {} - public function begin($mode = NULL) {} - public function commit() {} - public function rollback() {} - public function list_tables($like = NULL) {} - public function list_columns($table, $like = NULL, $add_prefix = TRUE) {} - public function escape($value) { return $value;} - - /** OVERRIDES **/ +abstract class Kohana_Database_LDAP extends Kohana_LDAP { + // LDAP doesnt use an identifier + protected $_identifier = ''; /** - * We override this parent function, since LDAP doesnt quote columns - * @note Override + * @defunct This required abstruct function is defunct for LDAP + */ + public function begin($mode = NULL) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); } + /** + * @defunct This required abstruct function is defunct for LDAP + */ + public function commit() { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); } + /** + * @defunct This required abstruct function is defunct for LDAP + */ + public function list_columns($table, $like = NULL, $add_prefix = TRUE) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); } + /** + * @defunct This required abstruct function is defunct for LDAP + */ + public function list_tables($like = NULL) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); } + /** + * @defunct This required abstruct function is defunct for LDAP + */ + public function query($type, $sql, $as_object = FALSE, array $params = NULL) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); } + /** + * @defunct This required abstruct function is defunct for LDAP + */ + public function rollback() { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); } + /** + * @defunct This required abstruct function is defunct for LDAP + */ + public function set_charset($charset) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); } + + /** REQUIRED ABSTRACT FUNCTIONS **/ + public function escape($value) { return $value;} + + /** + * @override We override this parent function, since LDAP doesnt quote columns */ public function quote_column($column) { return $column; } - /** Database_LDAP **/ + /** LDAP **/ /** - * @var boolean Whether we are full connected connection & bind + * Bind to the LDAP server with the creditials + * + * If we are successful, we return TRUE, if not FALSE + * + * @return boolean TRUE|FALSE */ - protected $_connected = FALSE; - - /** - * @var string Our default usage when connection - */ - public static $usage = 'default'; - private function _bind($u,$p) { /* // @todo To implement @@ -96,8 +115,12 @@ abstract class Kohana_Database_LDAP extends Database { /** * Bind to the LDAP server * - * @param string User DN to connect with, or blank for anonymous - * @param string Password for DN, or blank for anonymous + * If we have been passed a login_attr that is not DN, we'll try and find the + * DN to bind with. + * + * @param string User attribute to connect with, or blank for anonymous + * @param string Bind password to use with a DN or blank for anonymous + * @return mixed $this|FALSE */ public function bind($user,$pass) { // If we are already connected, no need to re-bind. @@ -125,17 +148,16 @@ abstract class Kohana_Database_LDAP extends Database { if ($this->_instance == 'auth') throw new Kohana_Exception('We shouldnt be authing an auth'); - $config = array( + $config = Arr::merge($this->_config,array( 'login_attr'=>'DN', - 'type'=>$this->_config['type'], 'connection'=>array( 'hostname'=>$hostname, 'port'=>$port, ), - ); + )); try { - $x = Database_LDAP::factory('auth',NULL,$config); + $x = LDAP::factory('auth',NULL,$config); // Our Auth Bind credentials are wrong if (! $x->bind($username,$password)) @@ -158,11 +180,8 @@ abstract class Kohana_Database_LDAP extends Database { foreach ($u as $base => $entries) foreach ($entries as $dn => $details) - if ($this->_bind($details['dn'],$pass)) { - // \xFF is a better delimiter, but the PHP driver uses underscore - $this->_connection_id = sha1($this->_instance.'_'.$details['dn']); + if ($this->_bind($dn,$pass)) return $this; - } // We didnt find an AUTH DN to bind with return FALSE; @@ -229,39 +248,31 @@ abstract class Kohana_Database_LDAP extends Database { $this->_connection = $r; } - public function connected() { - return ($this->_connection AND $this->_connected); - } + public function disconnect() { + try { + // Database is assumed disconnected + $status = TRUE; - /** - * A wrapper for parent::instance(), so that we can create multiple connections - * to the same LDAP server with different credentials/purposes. - * - * @param string A free form usage name, for this connection - * @param string A database configuration name, as per parent::instance() - * @param array An alternative database configuration to use for $name. - * @see Database::instance(); - */ - public static function factory($usage=NULL,$name=NULL,array $config=NULL) { - // Use the default instance name - if ($usage === NULL) - $usage = Database_LDAP::$usage; + if (is_resource($this->_connection)) { + if ($status = ldap_unbind($this->_connection)) { + // Clear the connection + $this->_connection = NULL; - if (! isset(Database::$instances[$usage])) { - // Use the default instance name - if ($name === NULL) - $name = Database::$default; + // Clear the instance + parent::disconnect(); + } + } - // Load the configuration for this database - if ($config === NULL) - $config = Kohana::$config->load('database')->$name; + } catch (Exception $e) { + // Database is probably not disconnected + $status = ! is_resource($this->_connection); } - return parent::instance($usage,$config); + return $status; } public function search($base=array()) { - return new Database_LDAP_Search($this->_connection,$base); + return new Database_LDAP_Search($this,$base); } } ?> diff --git a/classes/Kohana/Database/LDAP/Result.php b/classes/Kohana/Database/LDAP/Result.php new file mode 100644 index 0000000..296b180 --- /dev/null +++ b/classes/Kohana/Database/LDAP/Result.php @@ -0,0 +1,85 @@ +_total_rows = ldap_count_entries($result['l'],$result['r']); + $this->_current_entry = ldap_first_entry($result['l'],$result['r']); + + if (! $this->_total_rows) + $this->__destruct(); + } + + public function __destruct() { + if (is_resource($this->_result['r'])) + ldap_free_result($this->_result['r']); + } + + public function current() { + if ($this->_as_object === TRUE) { + // Return an stdClass + throw HTTP_Exception::factory(501,'Not implemented'); + + } elseif (is_string($this->_as_object)) { + // Return an object of given class name + throw HTTP_Exception::factory(501,'Not implemented'); + + } else { + // Return an array of the row + // @todo We could probably cache this for optimisation + $attrs = $vals = array(); + array_push($attrs,ldap_first_attribute($this->_result['l'],$this->_current_entry)); + + while ($x = ldap_next_attribute($this->_result['l'],$this->_current_entry)) + array_push($attrs,$x); + + foreach ($attrs as $attr) { + $vals[strtolower($attr)] = ldap_get_values($this->_result['l'],$this->_current_entry,$attr); + + // We dont need the count entry + unset($vals[strtolower($attr)]['count']); + } + + return $vals; + } + } + + /** + * Implements [Iterator::key], returns the dn. + * + * echo key($result); + * + * @return integer + */ + public function key() { + return ldap_get_dn($this->_result['l'],$this->_current_entry); + } + + public function seek($dn) { + $this->_current_entry = ldap_first_entry($this->_result['l'],$this->_result['r']); + + do { + if ($dn == $this->key()) + return TRUE; + + // Increment internal row for optimization assuming rows are fetched in order + $this->_current_entry = ldap_next_entry($this->_result['l'],$this->_current_entry); + + } while ($this->_current_entry); + + // We didnt find it + return FALSE; + } +} // End Database_LDAP_Result diff --git a/classes/Kohana/Database/LDAP/Search.php b/classes/Kohana/Database/LDAP/Search.php index a466082..7311a7c 100644 --- a/classes/Kohana/Database/LDAP/Search.php +++ b/classes/Kohana/Database/LDAP/Search.php @@ -4,14 +4,13 @@ * This class takes care of searching within LDAP * * @package Kohana/LDAP - * @subpackage LDAP/Search * @category Helpers * @author Deon George - * @copyright (c) phpLDAPadmin Development Team + * @copyright (c) 2013 phpLDAPadmin Development Team * @license http://dev.phpldapadmin.org/license.html */ abstract class Kohana_Database_LDAP_Search { - private $_connection; // Our LDAP Server to query + private $_db; // Our LDAP Server to query private $_attrs = array('*','+'); // LDAP Attributes to Return private $_base = array(); // LDAP Search Base @@ -22,6 +21,9 @@ abstract class Kohana_Database_LDAP_Search { private $_time_limit = '60'; // LDAP Search Time Limit private $_db_pending = array(); // LDAP Query Filter to compile + // Cache lifetime + protected $_lifetime = NULL; + /** * Callable database methods * @var array @@ -38,8 +40,8 @@ abstract class Kohana_Database_LDAP_Search { protected static $_properties = array( ); - public function __construct($resource,$base=array()) { - $this->_connection = $resource; + public function __construct(Database_LDAP $db,$base=array()) { + $this->_db = $db; $this->_base = is_null($base) ? $this->base() : $base; } @@ -108,7 +110,7 @@ abstract class Kohana_Database_LDAP_Search { if (! is_null($x=Kohana::$config->load('database.default.connection.database'))) return $x; - $x = Database_LDAP::factory('auth'); + $x = LDAP::factory('auth'); $u = $x->search(array('')) ->scope('base') @@ -116,13 +118,12 @@ abstract class Kohana_Database_LDAP_Search { // Remove the '' base $u = array_pop($u); - $u = array_pop($u); - return isset($u['namingcontexts']) ? $u['namingcontexts'] : array(); - } + // Quick validation + if ($u->count() > 1) + throw HTTP_Exception::factory(501,'We got more than 1 null DN?'); - public static function instance($resource) { - return new Database_LDAP_Search($resource); + return isset($u['']['namingcontexts']) ? $u['']['namingcontexts'] : array(); } public function deref($val) { @@ -134,7 +135,7 @@ abstract class Kohana_Database_LDAP_Search { /** * Search the LDAP database */ - public function run() { + public function run($as_object=FALSE,$object_params=NULL) { $query = array(); // Query Defaults @@ -146,72 +147,38 @@ abstract class Kohana_Database_LDAP_Search { $result = array(); foreach ($this->_base as $base) { - switch ($this->_scope) { - case 'base': - $search = ldap_read($this->_connection,$base,$this->_filter,$this->_attrs,$attrs_only,$this->_size_limit,$this->_time_limit,$this->_deref); - break; + $search = NULL; - case 'one': - $search = ldap_list($this->_connection,$base,$this->_filter,$this->_attrs,$attrs_only,$this->_size_limit,$this->_time_limit,$this->_deref); - break; + if ($this->_lifetime !== NULL AND $this->_db->caching()) { + // Set the cache key based on the database instance name and SQL + $cache_key = 'Database::query("'.$this->_db.'","'.$base.'","'.$this->_scope.'","'.$this->_filter.'")'; - case 'sub': - default: - $search = ldap_search($this->_connection,$base,$this->_filter,$this->_attrs,$attrs_only,$this->_size_limit,$this->_time_limit,$this->_deref); - break; - } - - if (! $search) { - $result[$base] = array(); - - continue; - } - - // Get the first entry identifier - if ($entries = ldap_get_entries($this->_connection,$search)) { - - # Remove the count - if (isset($entries['count'])) - unset($entries['count']); - - // Iterate over the entries - foreach ($entries as $a => $entry) { - if (! isset($entry['dn'])) - throw HTTP_Exception::factory(501,'No DN?'); - - // Remove the none entry references. - if (! is_array($entry)) { - unset($entries[$a]); - - continue; - } - - $dn = $entry['dn']; - unset($entry['dn']); - - // Iterate over the attributes - foreach ($entry as $b => $attrs) { - // Remove the none entry references. - if (! is_array($attrs)) { - unset($entry[$b]); - continue; - } - - // Remove the count - if (isset($entry[$b]['count'])) - unset($entry[$b]['count']); - } - - // Our queries always include the DN (the only value not an array). - $entry['dn'] = $dn; - $result[$base][$dn] = $entry; + // Read the cache first to delete a possible hit with lifetime <= 0 + if (($result = Kohana::cache($cache_key, NULL, $this->_lifetime)) !== NULL AND ! $this->_force_execute) { + // Return a cached result + $search = new Database_Result_Cached($result, array('b'=>$base,'s'=>$this->_scope,'f'=>$this->_filter), $as_object, $object_params); } } - // Sort our results - if (isset($result[$base])) - foreach ($result[$base] as $key => $values) - ksort($result[$base][$key]); + // Search is not cached, OR caching is disabled, so we'll query + if (! $search) { + switch ($this->_scope) { + case 'base': + $search = ldap_read($this->_db->connection(),$base,$this->_filter,$this->_attrs,$attrs_only,$this->_size_limit,$this->_time_limit,$this->_deref); + break; + + case 'one': + $search = ldap_list($this->_db->connection(),$base,$this->_filter,$this->_attrs,$attrs_only,$this->_size_limit,$this->_time_limit,$this->_deref); + break; + + case 'sub': + default: + $search = ldap_search($this->_db->connection(),$base,$this->_filter,$this->_attrs,$attrs_only,$this->_size_limit,$this->_time_limit,$this->_deref); + break; + } + + $result[$base] = new Database_LDAP_Result(array('l'=>$this->_db->connection(),'r'=>$search),array('b'=>$base,'s'=>$this->_scope,'f'=>$this->_filter),$as_object,$object_params); + } } return $result; diff --git a/classes/Kohana/Database/LDAP/Search/Builder/Query.php b/classes/Kohana/Database/LDAP/Search/Builder/Query.php index c2ea762..db81504 100644 --- a/classes/Kohana/Database/LDAP/Search/Builder/Query.php +++ b/classes/Kohana/Database/LDAP/Search/Builder/Query.php @@ -4,20 +4,21 @@ * This class takes care of building an LDAP filter query * * @package Kohana/LDAP - * @subpackage LDAP/Search * @category Helpers * @author Deon George - * @copyright (c) phpLDAPadmin Development Team + * @copyright (c) 2013 phpLDAPadmin Development Team * @license http://dev.phpldapadmin.org/license.html */ abstract class Kohana_Database_LDAP_Search_Builder_Query extends Database_Query_Builder { protected $_where = array(); - // @todo Not implemented + /** + * @defunct Not implemented + */ public function reset() {} public function __construct() { - parent::__construct(Database::SELECT,'ldap'); + parent::__construct(Database::SELECT,'LDAP'); } /** diff --git a/classes/Kohana/LDAP.php b/classes/Kohana/LDAP.php new file mode 100644 index 0000000..b86af8b --- /dev/null +++ b/classes/Kohana/LDAP.php @@ -0,0 +1,101 @@ +_config['caching']; + } + + /** + * Have we got a bound connection to the LDAP server + * + * @return boolean TRUE|FALSE + */ + public function connected() { + return ($this->_connection AND $this->_connected); + } + + /** + * Return the connection resource + * + * @return resource LDAP Resource Identifier + */ + public function connection() { + return $this->_connection; + } + + /** + * A wrapper for parent::instance(), so that we can create multiple connections + * to the same LDAP server with different credentials/purposes. + * + * Get a singleton Database instance. If configuration is not specified, + * it will be loaded from the database configuration file using the same + * group as the name. + * + * // Load the default database + * $db = LDAP::factory(); + * + * // Create a custom configured instance + * $db = LDAP:factory('auth','custom', $config); + * + * @param string A free form usage name, for this connection + * @param string A database configuration name, as per parent::instance() + * @param array An alternative database configuration to use for $name. + * @see Database::instance(); + * @return LDAP::instance(); + * + * @note We cant call this instance() like our parent, because of the additional parameter + * we need. + */ + public static function factory($usage=NULL,$name=NULL,array $config=NULL) { + // Use the default instance name + if ($usage === NULL) + $usage = LDAP::$usage; + + if (! isset(Database::$instances[$usage])) { + // Use the default instance name + if ($name === NULL) + $name = Database::$default; + + // Load the configuration for this database + if ($config === NULL) + $config = Kohana::$config->load('database')->$name; + } + + return parent::instance($usage,$config); + } + + /** + * @defunct This static function is defunct, you need to use factory() instead. + * @see LDAP::factory(); + */ + public static function instance($name=NULL,array $config=NULL) { + throw Kohana_Exception('Sorry, you cant use instance(), you need to use factory()'); + } +} +?> diff --git a/classes/LDAP.php b/classes/LDAP.php new file mode 100644 index 0000000..bcdcf3f --- /dev/null +++ b/classes/LDAP.php @@ -0,0 +1,4 @@ + diff --git a/config/database.php b/config/database.php index e52adab..aa26334 100644 --- a/config/database.php +++ b/config/database.php @@ -6,13 +6,13 @@ * @package Kohana/LDAP * @category Configuration * @author Deon George - * @copyright (c) phpLDAPadmin Development Team + * @copyright (c) 2013 phpLDAPadmin Development Team * @license http://dev.phpldapadmin.org/license.html */ return array ( 'default' => array ( - 'type' => 'ldap', + 'type' => 'LDAP', 'connection' => array( /** * The following options are available for MySQL: @@ -34,7 +34,7 @@ return array ( ), 'table_prefix' => NULL, 'charset' => 'utf8', - 'caching' => FALSE, + 'caching' => FALSE, // If this LDAP server is enabled for cache TRUE|FALSE 'profiling' => TRUE, 'login_attr'=>'uid', diff --git a/tests/classes/LDAPConnection.php b/tests/classes/LDAPConnection.php index 38109f6..9057372 100644 --- a/tests/classes/LDAPConnection.php +++ b/tests/classes/LDAPConnection.php @@ -28,11 +28,11 @@ Class LDAPConnection extends Unittest_TestCase { */ function testConnect($host,$port,$instance,$expect) { $connection = array( - 'type'=>'ldap', + 'type'=>'LDAP', 'connection'=>array('hostname'=>$host,'port'=>$port), ); - $x = Database_LDAP::factory($instance,NULL,$connection); + $x = LDAP::factory($instance,NULL,$connection); $x->connect(); if ($expect) @@ -80,7 +80,7 @@ Class LDAPConnection extends Unittest_TestCase { */ function testAuthConfiguration($user,$password,$expect) { $connection = array( - 'type'=>'ldap', + 'type'=>'LDAP', 'login_attr'=>'uid', 'connection'=>array( 'hostname'=>'localhost', @@ -91,10 +91,10 @@ Class LDAPConnection extends Unittest_TestCase { ); // Ensure we start with a clean auth connection. - Database_LDAP::factory('auth')->disconnect(); - Database_LDAP::factory('default')->disconnect(); + LDAP::factory('auth')->disconnect(); + LDAP::factory('default')->disconnect(); - $x = Database_LDAP::factory('default',NULL,$connection); + $x = LDAP::factory('default',NULL,$connection); $x->bind('bart','eatmyshorts'); if ($expect)