Initial Working KH Module for LDAP queries

This commit is contained in:
Deon George 2013-07-10 22:59:46 +10:00
commit 81aec2f376
10 changed files with 976 additions and 0 deletions

4
classes/Auth/LDAP.php Normal file
View File

@ -0,0 +1,4 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
class Auth_LDAP extends Kohana_Auth_LDAP {}
?>

View File

@ -0,0 +1,4 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
class Database_LDAP extends Kohana_Database_LDAP {}
?>

View File

@ -0,0 +1,4 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
class Database_LDAP_Search extends Kohana_Database_LDAP_Search {}
?>

View File

@ -0,0 +1,4 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
class Database_LDAP_Search_Builder_Query extends Kohana_Database_LDAP_Search_Builder_Query {}
?>

View File

@ -0,0 +1,85 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* LDAP Auth driver.
*
* @package Kohana/LDAP
* @subpackage Auth/LDAP
* @category Helpers
* @author Deon George
* @copyright (c) phpLDAPadmin Development Team
* @license http://dev.phpldapadmin.org/license.html
*/
class Kohana_Auth_LDAP extends Auth {
// Unnused required abstract functions
public function password($username) {}
public function check_password($password) {}
// Overrides
public function hash($str) {
// Since this is used automatically to encrypted a password, we need to suppress that for LDAP
if (! $this->_config['hash_key'])
return $str;
else
return parent::hash($str);
}
/**
* Logs a user in.
*
* @param string username
* @param string password
* @param boolean enable autologin (not supported)
* @return boolean
*/
protected function _login($user, $password, $remember) {
if ( ! is_object($user)) {
$username = $user;
// Load the user
// @todo Get the server ID
$user = Database_LDAP::factory('user')->bind($username,$password);
}
// @todo Implement conditional logging based on memberships to groups or other criteria.
// @todo This check of user being logged in needs to be better
if ($user) {
/*
// @todo To implement
if ($remember === TRUE) {
// Token data
$data = array(
'user_id'=>$user->id,
'expires'=>time()+$this->_config['lifetime'],
'user_agent'=>sha1(Request::$user_agent),
);
// Create a new autologin token
$token = ORM::factory('user_token')
->values($data)
->create();
// Set the autologin cookie
Cookie::set('authautologin', $token->token, $this->_config['lifetime']);
}
*/
// Finish the login
if (PHP_SAPI !== 'cli')
$this->complete_login($user);
return TRUE;
}
// Login failed
return FALSE;
}
public function logout($destroy=FALSE,$logout_all=FALSE) {
Database_LDAP::factory('user')->disconnect();
if (PHP_SAPI !== 'cli')
return parent::logout($destroy,$logout_all);
}
}
?>

View File

@ -0,0 +1,267 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class takes care of communicating with LDAP
*
* @package Kohana/LDAP
* @category Database/LDAP
* @author Deon George
* @copyright (c) 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 **/
/**
* We override this parent function, since LDAP doesnt quote columns
* @note Override
*/
public function quote_column($column) {
return $column;
}
/** Database_LDAP **/
/**
* @var boolean Whether we are full connected connection & bind
*/
protected $_connected = FALSE;
/**
* @var string Our default usage when connection
*/
public static $usage = 'default';
private function _bind($u,$p) {
/*
// @todo To implement
// If SASL has been configured for binding, then start it now.
if ($this->isSASLEnabled())
$br = $this->startSASL($this->_r,$method);
// Normal bind...
else
*/
if (Kohana::$profiling)
$benchmark = Profiler::start("Database Bind ({$this->_instance})", $this->_instance);
try {
$br = ldap_bind($this->_connection,$u,$p);
} catch (Exception $e) {
// This benchmark is worthless
if (isset($benchmark))
Profiler::delete($benchmark);
return FALSE;
}
if (! $br)
return FALSE;
$this->_connected = TRUE;
/*
// @todo To implement
# If this is a proxy session, we need to switch to the proxy user
if ($this->isProxyEnabled() && $bind['id'] && $method != 'anon')
if (! $this->startProxy($this->_r,$method)) {
$CACHE[$this->index][$method] = null;
}
*/
/*
// @todo To implement
if (function_exists('run_hook'))
run_hook('post_connect',array('server_id'=>$this->index,'method'=>$method,'id'=>$bind['id']));
*/
if (isset($benchmark))
Profiler::stop($benchmark);
return $br;
}
/**
* 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
*/
public function bind($user,$pass) {
// If we are already connected, no need to re-bind.
if ($this->_connected)
return $this;
// Make sure we are connected.
$this->_connection OR $this->connect();
// Do we need to do an search to find the DN
if (! empty($this->_config['login_attr']) AND strtoupper($this->_config['login_attr']) != 'DN') {
// Do we need to authenticate for this search?
// Extract the connection parameters, adding required variabels
extract($this->_config['connection'] + array(
'username' => '',
'password' => '',
'hostname' => '',
'port' => '',
));
// Prevent this information from showing up in traces
unset($this->_config['connection']['username'], $this->_config['connection']['password']);
// Sanity check
if ($this->_instance == 'auth')
throw new Kohana_Exception('We shouldnt be authing an auth');
$config = array(
'login_attr'=>'DN',
'type'=>$this->_config['type'],
'connection'=>array(
'hostname'=>$hostname,
'port'=>$port,
),
);
try {
$x = Database_LDAP::factory('auth',NULL,$config);
// Our Auth Bind credentials are wrong
if (! $x->bind($username,$password))
return FALSE;
$u = $x->search(NULL)
->scope('sub')
->where($this->_config['login_attr'],'=',$user)
->run();
if (! $u)
return FALSE;
} catch (Exception $e) {
// If we are a command line, we can just print the error
echo _('Unable to bind to LDAP server with CONFIG settings, please check them.');
echo _('The error message is').': '.$e->getMessage();
die();
}
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']);
return $this;
}
// We didnt find an AUTH DN to bind with
return FALSE;
}
// Bind
if ($this->_bind($user,$pass))
return $this;
else
return FALSE;
}
/**
* Create a connection to an LDAP server
*/
public function connect() {
if ($this->_connection)
return;
// Extract the connection parameters, adding required variabels
extract($this->_config['connection'] + array(
'hostname' => '',
'port' => '',
));
/*
// @todo To implement
if (function_exists('run_hook'))
run_hook('pre_connect',array('server_id'=>$this->index,'method'=>$method));
*/
// Benchmark this connection for the current instance
if (Kohana::$profiling)
$benchmark = Profiler::start("Database Connect ({$this->_instance})", $this->_instance);
$r = (! empty($this->_config['port'])) ? ldap_connect($hostname,$port) : ldap_connect($hostname);
if (! is_resource($r)) {
// This benchmark is worthless
if (isset($benchmark))
Profiler::delete($benchmark);
throw HTTP_Exception::factory(501,'UNHANDLED, $r is not a resource');
}
// Go with LDAP version 3 if possible (needed for renaming and Novell schema fetching)
ldap_set_option($r,LDAP_OPT_PROTOCOL_VERSION,3);
/* Disabling this makes it possible to browse the tree for Active Directory, and seems
* to not affect other LDAP servers (tested with OpenLDAP) as phpLDAPadmin explicitly
* specifies deref behavior for each ldap_search operation. */
ldap_set_option($r,LDAP_OPT_REFERRALS,0);
/*
// @todo To implement
# Try to fire up TLS is specified in the config
if ($this->isTLSEnabled())
$this->startTLS($this->_connection);
*/
if (isset($benchmark))
Profiler::stop($benchmark);
$this->_connection = $r;
}
public function connected() {
return ($this->_connection AND $this->_connected);
}
/**
* 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 (! 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);
}
public function search($base=array()) {
return new Database_LDAP_Search($this->_connection,$base);
}
}
?>

View File

@ -0,0 +1,246 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class takes care of searching within LDAP
*
* @package Kohana/LDAP
* @subpackage LDAP/Search
* @category Helpers
* @author Deon George
* @copyright (c) phpLDAPadmin Development Team
* @license http://dev.phpldapadmin.org/license.html
*/
abstract class Kohana_Database_LDAP_Search {
private $_connection; // Our LDAP Server to query
private $_attrs = array('*','+'); // LDAP Attributes to Return
private $_base = array(); // LDAP Search Base
private $_deref = LDAP_DEREF_NEVER; // LDAP Search Default DEREF
private $_filter = '(objectclass=*)'; // LDAP Search Filter
private $_size_limit = '500'; // LDAP Search Size Limit
private $_scope = 'base'; // LDAP Search Scope
private $_time_limit = '60'; // LDAP Search Time Limit
private $_db_pending = array(); // LDAP Query Filter to compile
/**
* Callable database methods
* @var array
*/
protected static $_db_methods = array(
'where', 'and_where', 'or_where', 'where_open', 'and_where_open', 'or_where_open', 'where_close',
'and_where_close', 'or_where_close',
);
/**
* Members that have access methods
* @var array
*/
protected static $_properties = array(
);
public function __construct($resource,$base=array()) {
$this->_connection = $resource;
$this->_base = is_null($base) ? $this->base() : $base;
}
/**
* Handles pass-through to database methods. Calls to query methods
* (query, get, insert, update) are not allowed. Query builder methods
* are chainable.
*
* @param string $method Method name
* @param array $args Method arguments
* @return mixed
*/
public function __call($method,array $args) {
if (in_array($method,Database_LDAP_Search::$_properties)) {
/*
// @todo To Implement
if ($method === 'validation')
{
if ( ! isset($this->_validation))
{
// Initialize the validation object
$this->_validation();
}
}
*/
// Return the property
return $this->{'_'.$method};
}
elseif (in_array($method,Database_LDAP_Search::$_db_methods))
{
// Add pending database call which is executed after query type is determined
$this->_db_pending[] = array('name' => $method,'args' => $args);
return $this;
}
else
{
throw new Kohana_Exception('Invalid method :method called in :class',
array(':method' => $method,':class' => get_class($this)));
}
}
private function _build() {
$search = new Database_LDAP_Search_Builder_Query();
// Process pending database method calls
foreach ($this->_db_pending as $method) {
$name = $method['name'];
$args = $method['args'];
$this->_db_applied[$name] = $name;
call_user_func_array(array($search,$name),$args);
}
return $search;
}
/**
* Figure out the bases
*/
public function base() {
// If the base is set in the configuration file, then just return that.
if (! is_null($x=Kohana::$config->load('database.default.connection.database')))
return $x;
$x = Database_LDAP::factory('auth');
$u = $x->search(array(''))
->scope('base')
->run();
// Remove the '' base
$u = array_pop($u);
$u = array_pop($u);
return isset($u['namingcontexts']) ? $u['namingcontexts'] : array();
}
public static function instance($resource) {
return new Database_LDAP_Search($resource);
}
public function deref($val) {
$this->_deref = $val;
return $this;
}
/**
* Search the LDAP database
*/
public function run() {
$query = array();
// Query Defaults
$attrs_only = 0;
// Compile our query
if ($this->_db_pending)
$this->_filter = $this->_build();
$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;
case 'one':
$search = ldap_list($this->_connection,$base,$this->_filter,$this->_attrs,$attrs_only,$this->_size_limit,$this->_time_limit,$this->_deref);
break;
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;
}
}
// Sort our results
if (isset($result[$base]))
foreach ($result[$base] as $key => $values)
ksort($result[$base][$key]);
}
return $result;
}
public function size_limit($val) {
$this->_size_limit = $val;
return $this;
}
public function scope($val) {
switch ($val) {
case 'base':
case 'sub':
case 'one': $this->_scope = $val;
break;
default:
throw new Kohana_Exception('Unknown search scope :scope',array(':scope',$val));
}
return $this;
}
public function time_limit($val) {
$this->_time_limit = $val;
return $this;
}
}
?>

View File

@ -0,0 +1,214 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* 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
* @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
public function reset() {}
public function __construct() {
parent::__construct(Database::SELECT,'ldap');
}
/**
* Alias of and_where()
*
* @param mixed column name or array($column, $alias) or object
* @param string logic operator
* @param mixed column value
* @return $this
*/
public function where($column,$op,$value) {
return $this->and_where($column,$op,$value);
}
/**
* Creates a new "AND WHERE" condition for the query.
*
* @param mixed column name or array($column,$alias) or object
* @param string logic operator
* @param mixed column value
* @return $this
*/
public function and_where($column,$op,$value) {
$this->_where[] = array('AND' => array($column,$op,$value));
return $this;
}
/**
* Creates a new "OR WHERE" condition for the query.
*
* @param mixed column name or array($column,$alias) or object
* @param string logic operator
* @param mixed column value
* @return $this
*/
public function or_where($column,$op,$value) {
$this->_where[] = array('OR' => array($column,$op,$value));
return $this;
}
/**
* Alias of and_where_open()
*
* @return $this
*/
public function where_open() {
return $this->and_where_open();
}
/**
* Opens a new "AND WHERE (...)" grouping.
*
* @return $this
*/
public function and_where_open() {
$this->_where[] = array('AND' => '(');
return $this;
}
/**
* Opens a new "OR WHERE (...)" grouping.
*
* @return $this
*/
public function or_where_open() {
$this->_where[] = array('OR' => '(');
return $this;
}
public function compile($db = NULL) {
$filter = '';
return $this->_compile_conditions($db,$this->_where);
}
/**
* Closes an open "AND WHERE (...)" grouping.
*
* @return $this
*/
public function where_close() {
return $this->and_where_close();
}
/**
* Closes an open "AND WHERE (...)" grouping.
*
* @return $this
*/
public function and_where_close() {
$this->_where[] = array('AND' => ')');
return $this;
}
/**
* Closes an open "OR WHERE (...)" grouping.
*
* @return $this
*/
public function or_where_close() {
$this->_where[] = array('OR' => ')');
return $this;
}
/**
* Compiles an array of conditions into an LDAP filter.
*
* @param object Database instance
* @param array condition statements
* @return string
*/
protected function _compile_conditions(Database $db,array $conditions,$index=0) {
$current_condition = $last_condition = NULL;
$filter = '';
$sub = 0;
foreach ($conditions as $key => $group) {
// If we have been called again, we need to skip ahead, or skip what has been processed
if ($key < $index OR $sub)
continue;
// Process groups of conditions
foreach ($group as $logic => $condition) {
if ($condition === '(') {
$filter .= $this->_compile_conditions($db,$conditions,$key+1);
$sub = 1;
} elseif ($condition === ')') {
if ($index) {
// As we return, we'll include our condition
switch ($current_condition) {
case 'AND':
return '(&'.$filter.')';
case 'OR':
return '(|'.$filter.')';
default:
throw new Kohana_Exception('Condition :condition not handled.',array(':condition'=>$condition));
}
}
$sub = 0;
} else {
// We currently cant handle when a condition changes, without brackets.
if ($filter AND $current_condition AND $current_condition != $logic)
throw new Kohana_Exception('Condition changed without brackets');
$current_condition = $logic;
// Split the condition
list($column,$op,$value) = $condition;
// Database operators are always uppercase
$op = strtoupper($op);
if ((is_string($value) AND array_key_exists($value,$this->_parameters)) === FALSE) {
// Quote the value, it is not a parameter
$value = $db->quote($value);
}
if ($column) {
// Apply proper quoting to the column
$column = $db->quote_column($column);
}
// Append the statement to the query
$filter .= trim('('.$column.$op.$value.')');
}
$last_condition = $condition;
}
}
switch ($current_condition) {
case 'AND':
return '(&'.$filter.')';
case 'OR':
return '(|'.$filter.')';
default:
throw new Kohana_Exception('Condition :condition not handled.',array(':condition'=>$condition));
}
}
}
?>

43
config/database.php Normal file
View File

@ -0,0 +1,43 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* LDAP Configuration - LDAP Server Definitions
*
* @package Kohana/LDAP
* @category Configuration
* @author Deon George
* @copyright (c) phpLDAPadmin Development Team
* @license http://dev.phpldapadmin.org/license.html
*/
return array (
'default' => array (
'type' => 'ldap',
'connection' => array(
/**
* The following options are available for MySQL:
*
* string hostname server hostname, or socket
* string database our basedn
* string username database username
* string password database password
* boolean persistent use persistent connections?
*
* Ports and sockets may be appended to the hostname.
*/
'hostname' => 'localhost',
'database' => NULL,
'port' => 389,
'username' => FALSE,
'password' => FALSE,
'persistent' => NULL,
),
'table_prefix' => NULL,
'charset' => 'utf8',
'caching' => FALSE,
'profiling' => TRUE,
'login_attr'=>'uid',
),
);
?>

View File

@ -0,0 +1,105 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This should test all our connection methods to the LDAP server
* and return success and failures.
*
* @package Kohana/LDAP
* @category Test
* @author Deon George
* @copyright (c) 2013 phpLDAPadmin Development Team
* @license http://dev.phpldapadmin.org/license.html
* @group ldap
* @group ldap.server
*/
Class LDAPConnection extends Unittest_TestCase {
function hosts() {
return array(
array('localhost','389','a',TRUE),
array('localhost','389','b',TRUE),
array('localhost','390','a',FALSE),
array('localhost','390','b',FALSE),
);
}
/**
* Test that we can connect to an LDAP server
* @dataProvider hosts
*/
function testConnect($host,$port,$instance,$expect) {
$connection = array(
'type'=>'ldap',
'connection'=>array('hostname'=>$host,'port'=>$port),
);
$x = Database_LDAP::factory($instance,NULL,$connection);
$x->connect();
if ($expect)
$this->assertAttributeInternalType('resource','_connection',$x);
// In OpenLDAP, this still returns a resource, even though it should be a failure.
else
$this->assertAttributeInternalType('resource','_connection',$x);
$x->disconnect();
}
function auths() {
return array(
array('bart','eatmyshorts',TRUE),
array('bart','Eatmyshorts',FALSE),
array('nobart','Eatmyshorts',FALSE),
);
}
/**
* @dataProvider auths
* @depends testConnect
*/
function testAuth($user,$password,$expect) {
if ($expect)
$this->assertTrue(Auth::instance()->login($user,$password));
else
$this->assertFalse(Auth::instance()->login($user,$password));
Auth::instance()->logout();
}
function authconfig() {
return array(
array('','',TRUE),
array('bart','x',FALSE),
array('cn=Manager','Eatmyshorts',FALSE),
array('cn=Manager,dc=example.com','NotAllowed',TRUE),
);
}
/**
* @dataProvider authconfig
* @depends testConnect
*/
function testAuthConfiguration($user,$password,$expect) {
$connection = array(
'type'=>'ldap',
'login_attr'=>'uid',
'connection'=>array(
'hostname'=>'localhost',
'port'=>389,
'username'=>$user,
'password'=>$password
),
);
// Ensure we start with a clean auth connection.
Database_LDAP::factory('auth')->disconnect();
Database_LDAP::factory('default')->disconnect();
$x = Database_LDAP::factory('default',NULL,$connection);
$x->bind('bart','eatmyshorts');
if ($expect)
$this->assertTrue($x->connected());
else
$this->assertFalse($x->connected());
}
}