LdapUserBackend: Extend Repository and implement UserBackendInterface

refs 
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
application/forms/Config/Authentication
library/Icinga/Authentication/User
modules/setup/application/forms
test/php/application/forms/Config/Authentication

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

@ -3,29 +3,55 @@
namespace Icinga\Authentication\User;
use Icinga\User;
use Icinga\Protocol\Ldap\Query;
use Icinga\Protocol\Ldap\Connection;
use Icinga\Data\ConfigObject;
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\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;
/**
* The objectClass where look for users
*
* @var string
*/
protected $userClass;
/**
* The attribute name where to find a user's name
*
* @var string
*/
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;
@ -41,20 +67,115 @@ class LdapUserBackend extends UserBackend
'samaccountname' => 'sAMAccountName'
);
public function __construct(
Connection $conn,
$userClass,
$userNameAttribute,
$baseDn,
$cutomFilter,
$groupOptions = null
) {
$this->conn = $conn;
$this->baseDn = trim($baseDn) ?: $conn->getDN();
$this->userClass = $this->getNormedAttribute($userClass);
/**
* Set the base DN to use for a query
*
* @param string $baseDn
*
* @return $this
*/
public function setBaseDn($baseDn)
{
if (($baseDn = trim($baseDn))) {
$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->customFilter = trim($cutomFilter);
$this->groupOptions = $groupOptions;
return $this;
}
/**
* 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(
$this->userClass,
array(
$this->userNameAttribute
)
);
return $this
->setBaseDn($config->base_dn)
->setUserClass($config->user_class)
->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;
}
/**
* Create a query filtered by the given username
* Initialize this repository's query columns
*
* @param string $username
*
* @return Query
* @throws ProgrammingError In case either $this->userNameAttribute or $this->userClass has not been set yet
*/
protected function selectUser($username)
protected function initializeQueryColumns()
{
return $this->selectUsers()->setUsePagedResults(false)->where(
$this->userNameAttribute,
str_replace('*', '', $username)
);
if ($this->queryColumns === null) {
if ($this->userClass === null) {
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
*
* 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>
* <li>Connection credentials are correct and the bind is possible</li>
* <li>At least one user exists</li>
@ -125,23 +270,23 @@ class LdapUserBackend extends UserBackend
public function assertAuthenticationPossible()
{
try {
$result = $this->selectUsers()->fetchRow();
$result = $this->select()->fetchRow();
} catch (LdapException $e) {
throw new AuthenticationException('Connection not possible.', $e);
}
if ($result === null) {
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->baseDn,
$this->customFilter ?: 'None'
$this->baseDn ?: $this->ds->getDn(),
$this->filter ?: 'None'
);
}
if (! isset($result->{$this->userNameAttribute})) {
if (! isset($result->user_name)) {
throw new AuthenticationException(
'UserNameAttribute "%s" not existing in objectClass="%s"',
'UserNameAttribute "%s" not existing in objectClass "%s"',
$this->userNameAttribute,
$this->userClass
);
@ -163,7 +308,7 @@ class LdapUserBackend extends UserBackend
return array();
}
$q = $this->conn->select()
$result = $this->ds->select()
->setBase($this->groupOptions['group_base_dn'])
->from(
$this->groupOptions['group_class'],
@ -172,12 +317,10 @@ class LdapUserBackend extends UserBackend
->where(
$this->groupOptions['group_member_attribute'],
$dn
);
$result = $this->conn->fetchAll($q);
)
->fetchAll();
$groups = array();
foreach ($result as $group) {
$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 string $password
* @param boolean $healthCheck Assert that authentication is possible at all
* @param User $user
* @param string $password
*
* @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 {
$this->assertAuthenticationPossible();
} catch (AuthenticationException $e) {
throw new AuthenticationException(
'Authentication against backend "%s" not possible.',
$this->getName(),
$e
);
}
}
try {
$userDn = $this->conn->fetchDN($this->selectUser($user->getUsername()));
$userDn = $this
->select()
->where('user_name', str_replace('*', '', $user->getUsername()))
->getQuery()
->setUsePagedResults(false)
->fetchDn();
if ($userDn === null) {
return false;
}
$authenticated = $this->conn->testCredentials(
$userDn,
$password
);
$authenticated = $this->ds->testCredentials($userDn, $password);
if ($authenticated) {
$groups = $this->getGroups($userDn);
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;
}
}

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

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

@ -29,7 +29,8 @@ class LdapBackendFormTest extends BaseTestCase
{
$this->setUpResourceFactoryMock();
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...
$form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));