LdapUserBackend: Extend Repository and implement UserBackendInterface

refs #8826
This commit is contained in:
Johannes Meyer 2015-05-04 12:18:25 +02:00
parent e74194c18e
commit c441117324
5 changed files with 231 additions and 157 deletions

View File

@ -170,13 +170,8 @@ class LdapBackendForm extends Form
public static function isValidAuthenticationBackend(Form $form) public static function isValidAuthenticationBackend(Form $form)
{ {
try { try {
$ldapUserBackend = new LdapUserBackend( $ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig()));
ResourceFactory::createResource($form->getResourceConfig()), $ldapUserBackend->setConfig(new ConfigObject($form->getValues()));
$form->getElement('user_class')->getValue(),
$form->getElement('user_name_attribute')->getValue(),
$form->getElement('base_dn')->getValue(),
$form->getElement('filter')->getValue()
);
$ldapUserBackend->assertAuthenticationPossible(); $ldapUserBackend->assertAuthenticationPossible();
} catch (AuthenticationException $e) { } catch (AuthenticationException $e) {
if (($previous = $e->getPrevious()) !== null) { if (($previous = $e->getPrevious()) !== null) {

View File

@ -3,29 +3,55 @@
namespace Icinga\Authentication\User; namespace Icinga\Authentication\User;
use Icinga\User; use Icinga\Data\ConfigObject;
use Icinga\Protocol\Ldap\Query;
use Icinga\Protocol\Ldap\Connection;
use Icinga\Exception\AuthenticationException; use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ProgrammingError;
use Icinga\Repository\Repository;
use Icinga\Repository\RepositoryQuery;
use Icinga\Protocol\Ldap\Exception as LdapException; use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression; use Icinga\Protocol\Ldap\Expression;
use Icinga\User;
class LdapUserBackend extends UserBackend class LdapUserBackend extends Repository implements UserBackendInterface
{ {
/** /**
* Connection to the LDAP server * The base DN to use for a query
* *
* @var Connection * @var string
*/ */
protected $conn;
protected $baseDn; protected $baseDn;
/**
* The objectClass where look for users
*
* @var string
*/
protected $userClass; protected $userClass;
/**
* The attribute name where to find a user's name
*
* @var string
*/
protected $userNameAttribute; protected $userNameAttribute;
protected $customFilter; /**
* The custom LDAP filter to apply on search queries
*
* @var string
*/
protected $filter;
/**
* The default sort rules to be applied on a query
*
* @var array
*/
protected $sortRules = array(
'user_name' => array(
'order' => 'asc'
)
);
protected $groupOptions; protected $groupOptions;
@ -41,20 +67,115 @@ class LdapUserBackend extends UserBackend
'samaccountname' => 'sAMAccountName' 'samaccountname' => 'sAMAccountName'
); );
public function __construct( /**
Connection $conn, * Set the base DN to use for a query
$userClass, *
$userNameAttribute, * @param string $baseDn
$baseDn, *
$cutomFilter, * @return $this
$groupOptions = null */
) { public function setBaseDn($baseDn)
$this->conn = $conn; {
$this->baseDn = trim($baseDn) ?: $conn->getDN(); if (($baseDn = trim($baseDn))) {
$this->userClass = $this->getNormedAttribute($userClass); $this->baseDn = $baseDn;
}
return $this;
}
/**
* Return the base DN to use for a query
*
* @return string
*/
public function getBaseDn()
{
return $this->baseDn;
}
/**
* Set the objectClass where to look for users
*
* Sets also the base table name for the underlying repository.
*
* @param string $userClass
*
* @return $this
*/
public function setUserClass($userClass)
{
$this->baseTable = $this->userClass = $this->getNormedAttribute($userClass);
return $this;
}
/**
* Return the objectClass where to look for users
*
* @return string
*/
public function getUserClass()
{
return $this->userClass;
}
/**
* Set the attribute name where to find a user's name
*
* @param string $userNameAttribute
*
* @return $this
*/
public function setUserNameAttribute($userNameAttribute)
{
$this->userNameAttribute = $this->getNormedAttribute($userNameAttribute); $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute);
$this->customFilter = trim($cutomFilter); return $this;
$this->groupOptions = $groupOptions; }
/**
* Return the attribute name where to find a user's name
*
* @return string
*/
public function getUserNameAttribute()
{
return $this->userNameAttribute;
}
/**
* Set the custom LDAP filter to apply on search queries
*
* @param string $filter
*
* @return $this
*/
public function setFilter($filter)
{
if (($filter = trim($filter))) {
$this->filter = $filter;
}
return $this;
}
/**
* Return the custom LDAP filter to apply on search queries
*
* @return string
*/
public function getFilter()
{
return $this->filter;
}
public function setGroupOptions(array $options)
{
$this->groupOptions = $options;
return $this;
}
public function getGroupOptions()
{
return $this->groupOptions;
} }
/** /**
@ -75,45 +196,69 @@ class LdapUserBackend extends UserBackend
} }
/** /**
* Create a query to select all usernames * Apply the given configuration to this backend
* *
* @return Query * @param ConfigObject $config
*
* @return $this
*/ */
protected function selectUsers() public function setConfig(ConfigObject $config)
{ {
$query = $this->conn->select()->setBase($this->baseDn)->from( return $this
$this->userClass, ->setBaseDn($config->base_dn)
array( ->setUserClass($config->user_class)
$this->userNameAttribute ->setUserNameAttribute($config->user_name_attribute)
) ->setFilter($config->filter);
); }
if ($this->customFilter) { /**
$query->addFilter(new Expression($this->customFilter)); * Return a new query for the given columns
*
* @param array $columns The desired columns, if null all columns will be queried
*
* @return RepositoryQuery
*/
public function select(array $columns = null)
{
$this->initializeQueryColumns();
$query = parent::select($columns);
$query->getQuery()->setBase($this->baseDn);
if ($this->filter) {
$query->getQuery()->where(new Expression($this->filter));
} }
return $query; return $query;
} }
/** /**
* Create a query filtered by the given username * Initialize this repository's query columns
* *
* @param string $username * @throws ProgrammingError In case either $this->userNameAttribute or $this->userClass has not been set yet
*
* @return Query
*/ */
protected function selectUser($username) protected function initializeQueryColumns()
{ {
return $this->selectUsers()->setUsePagedResults(false)->where( if ($this->queryColumns === null) {
$this->userNameAttribute, if ($this->userClass === null) {
str_replace('*', '', $username) throw new ProgrammingError('It is required to set the objectClass where to look for users first');
}
if ($this->userNameAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first');
}
$this->queryColumns[$this->userClass] = array(
'user_name' => $this->userNameAttribute,
'is_active' => 'unknown', // msExchUserAccountControl == 2/512/514? <- AD LDAP
'created_at' => 'whenCreated', // That's AD LDAP,
'last_modified' => 'whenChanged' // what's OpenLDAP?
); );
} }
}
/** /**
* Probe the backend to test if authentication is possible * Probe the backend to test if authentication is possible
* *
* Try to bind to the backend and query all available users to check if: * Try to bind to the backend and fetch a single user to check if:
* <ul> * <ul>
* <li>Connection credentials are correct and the bind is possible</li> * <li>Connection credentials are correct and the bind is possible</li>
* <li>At least one user exists</li> * <li>At least one user exists</li>
@ -125,23 +270,23 @@ class LdapUserBackend extends UserBackend
public function assertAuthenticationPossible() public function assertAuthenticationPossible()
{ {
try { try {
$result = $this->selectUsers()->fetchRow(); $result = $this->select()->fetchRow();
} catch (LdapException $e) { } catch (LdapException $e) {
throw new AuthenticationException('Connection not possible.', $e); throw new AuthenticationException('Connection not possible.', $e);
} }
if ($result === null) { if ($result === null) {
throw new AuthenticationException( throw new AuthenticationException(
'No objects with objectClass="%s" in DN="%s" found. (Filter: %s)', 'No objects with objectClass "%s" in DN "%s" found. (Filter: %s)',
$this->userClass, $this->userClass,
$this->baseDn, $this->baseDn ?: $this->ds->getDn(),
$this->customFilter ?: 'None' $this->filter ?: 'None'
); );
} }
if (! isset($result->{$this->userNameAttribute})) { if (! isset($result->user_name)) {
throw new AuthenticationException( throw new AuthenticationException(
'UserNameAttribute "%s" not existing in objectClass="%s"', 'UserNameAttribute "%s" not existing in objectClass "%s"',
$this->userNameAttribute, $this->userNameAttribute,
$this->userClass $this->userClass
); );
@ -163,7 +308,7 @@ class LdapUserBackend extends UserBackend
return array(); return array();
} }
$q = $this->conn->select() $result = $this->ds->select()
->setBase($this->groupOptions['group_base_dn']) ->setBase($this->groupOptions['group_base_dn'])
->from( ->from(
$this->groupOptions['group_class'], $this->groupOptions['group_class'],
@ -172,12 +317,10 @@ class LdapUserBackend extends UserBackend
->where( ->where(
$this->groupOptions['group_member_attribute'], $this->groupOptions['group_member_attribute'],
$dn $dn
); )
->fetchAll();
$result = $this->conn->fetchAll($q);
$groups = array(); $groups = array();
foreach ($result as $group) { foreach ($result as $group) {
$groups[] = $group->{$this->groupOptions['group_attribute']}; $groups[] = $group->{$this->groupOptions['group_attribute']};
} }
@ -186,40 +329,30 @@ class LdapUserBackend extends UserBackend
} }
/** /**
* Return whether the given user credentials are valid * Authenticate the given user
* *
* @param User $user * @param User $user
* @param string $password * @param string $password
* @param boolean $healthCheck Assert that authentication is possible at all
* *
* @return bool * @return bool True on success, false on failure
* *
* @throws AuthenticationException In case an error occured or the health check has failed * @throws AuthenticationException In case authentication is not possible due to an error
*/ */
public function authenticate(User $user, $password, $healthCheck = false) public function authenticate(User $user, $password)
{ {
if ($healthCheck) {
try { try {
$this->assertAuthenticationPossible(); $userDn = $this
} catch (AuthenticationException $e) { ->select()
throw new AuthenticationException( ->where('user_name', str_replace('*', '', $user->getUsername()))
'Authentication against backend "%s" not possible.', ->getQuery()
$this->getName(), ->setUsePagedResults(false)
$e ->fetchDn();
);
}
}
try {
$userDn = $this->conn->fetchDN($this->selectUser($user->getUsername()));
if ($userDn === null) { if ($userDn === null) {
return false; return false;
} }
$authenticated = $this->conn->testCredentials( $authenticated = $this->ds->testCredentials($userDn, $password);
$userDn,
$password
);
if ($authenticated) { if ($authenticated) {
$groups = $this->getGroups($userDn); $groups = $this->getGroups($userDn);
if ($groups !== null) { if ($groups !== null) {
@ -237,35 +370,4 @@ class LdapUserBackend extends UserBackend
); );
} }
} }
/**
* Get the number of users available
*
* @return int
*/
public function count()
{
return $this->selectUsers()->count();
}
/**
* Return the names of all available users
*
* @return array
*/
public function listUsers()
{
$users = array();
foreach ($this->selectUsers()->fetchAll() as $row) {
if (is_array($row->{$this->userNameAttribute})) {
foreach ($row->{$this->userNameAttribute} as $col) {
$users[] = $col;
}
} else {
$users[] = $row->{$this->userNameAttribute};
}
}
return $users;
}
} }

View File

@ -160,49 +160,30 @@ class UserBackend
$backend = new DbUserBackend($resource); $backend = new DbUserBackend($resource);
break; break;
case 'msldap': case 'msldap':
$groupOptions = array( $backend = new LdapUserBackend($resource);
$backend->setBaseDn($backendConfig->base_dn);
$backend->setUserClass($backendConfig->get('user_class', 'user'));
$backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName'));
$backend->setFilter($backendConfig->filter);
$backend->setGroupOptions(array(
'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()), 'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()),
'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'), 'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'),
'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'), 'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'),
'group_class' => $backendConfig->get('group_class', 'group') 'group_class' => $backendConfig->get('group_class', 'group')
); ));
$backend = new LdapUserBackend(
$resource,
$backendConfig->get('user_class', 'user'),
$backendConfig->get('user_name_attribute', 'sAMAccountName'),
$backendConfig->get('base_dn', $resource->getDN()),
$backendConfig->get('filter'),
$groupOptions
);
break; break;
case 'ldap': case 'ldap':
if ($backendConfig->user_class === null) { $backend = new LdapUserBackend($resource);
throw new ConfigurationError( $backend->setBaseDn($backendConfig->base_dn);
'Authentication configuration for backend "%s" is missing the \'user_class\' directive', $backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson'));
$name $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid'));
); $backend->setFilter($backendConfig->filter);
} $backend->setGroupOptions(array(
if ($backendConfig->user_name_attribute === null) {
throw new ConfigurationError(
'Authentication configuration for backend "%s" is'
. ' missing the \'user_name_attribute\' directive',
$name
);
}
$groupOptions = array(
'group_base_dn' => $backendConfig->group_base_dn, 'group_base_dn' => $backendConfig->group_base_dn,
'group_attribute' => $backendConfig->group_attribute, 'group_attribute' => $backendConfig->group_attribute,
'group_member_attribute' => $backendConfig->group_member_attribute, 'group_member_attribute' => $backendConfig->group_member_attribute,
'group_class' => $backendConfig->group_class 'group_class' => $backendConfig->group_class
); ));
$backend = new LdapUserBackend(
$resource,
$backendConfig->user_class,
$backendConfig->user_name_attribute,
$backendConfig->get('base_dn', $resource->getDN()),
$backendConfig->get('filter'),
$groupOptions
);
break; break;
} }

View File

@ -268,13 +268,8 @@ class AdminAccountPage extends Form
if ($this->backendConfig['backend'] === 'db') { if ($this->backendConfig['backend'] === 'db') {
$backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig))); $backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
} elseif ($this->backendConfig['backend'] === 'ldap') { } elseif ($this->backendConfig['backend'] === 'ldap') {
$backend = new LdapUserBackend( $backend = new LdapUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
ResourceFactory::createResource(new ConfigObject($this->resourceConfig)), $backend->setConfig($this->backendConfig);
$this->backendConfig['user_class'],
$this->backendConfig['user_name_attribute'],
$this->backendConfig['base_dn'],
$this->backendConfig['filter']
);
} else { } else {
throw new LogicException( throw new LogicException(
sprintf( sprintf(

View File

@ -29,7 +29,8 @@ class LdapBackendFormTest extends BaseTestCase
{ {
$this->setUpResourceFactoryMock(); $this->setUpResourceFactoryMock();
Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend') Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
->shouldReceive('assertAuthenticationPossible')->andReturnNull(); ->shouldReceive('assertAuthenticationPossible')->andReturnNull()
->shouldReceive('setConfig')->andReturnNull();
// Passing array(null) is required to make Mockery call the constructor... // Passing array(null) is required to make Mockery call the constructor...
$form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null)); $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));