Merge branch 'feature/ldap-user-group-backend-7343'

resolves #7343
This commit is contained in:
Johannes Meyer 2015-06-05 16:27:01 +02:00
commit 8d845767ac
10 changed files with 1137 additions and 153 deletions

View File

@ -52,7 +52,7 @@ class UsergroupbackendController extends Controller
$form->setIniConfig(Config::app('groups'));
$form->setOnSuccess(function (UserGroupBackendForm $form) {
try {
$form->add($form->getValues());
$form->add(array_filter($form->getValues()));
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
@ -85,7 +85,12 @@ class UsergroupbackendController extends Controller
$form->setIniConfig(Config::app('groups'));
$form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) {
try {
$form->edit($backendName, $form->getValues());
$form->edit($backendName, array_map(
function ($v) {
return $v !== '' ? $v : null;
},
$form->getValues()
));
} catch (Exception $e) {
$form->error($e->getMessage());
return false;

View File

@ -0,0 +1,305 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Icinga\Application\Config;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Protocol\Ldap\Connection;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form for managing LDAP user group backends
*/
class LdapUserGroupBackendForm extends Form
{
/**
* Initialize this form
*/
public function init()
{
$this->setName('form_config_ldapusergroupbackend');
}
/**
* Create and add elements to this form
*
* @param array $formData
*/
public function createElements(array $formData)
{
$resourceNames = $this->getLdapResourceNames();
$this->addElement(
'select',
'resource',
array(
'required' => true,
'autosubmit' => true,
'label' => $this->translate('LDAP Connection'),
'description' => $this->translate('The LDAP connection to use for this backend.'),
'multiOptions' => array_combine($resourceNames, $resourceNames)
)
);
$resource = ResourceFactory::create(
isset($formData['resource']) && in_array($formData['resource'], $resourceNames)
? $formData['resource']
: $resourceNames[0]
);
$userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend'));
$userBackendNames = $this->getLdapUserBackendNames($resource);
if (! empty($userBackendNames)) {
$userBackends = array_merge($userBackends, array_combine($userBackendNames, $userBackendNames));
}
$this->addElement(
'select',
'user_backend',
array(
'required' => true,
'autosubmit' => true,
'label' => $this->translate('User Backend'),
'description' => $this->translate('The user backend to link with this user group backend.'),
'multiOptions' => $userBackends
)
);
$groupBackend = new LdapUserGroupBackend($resource);
if ($formData['type'] === 'ldap') {
$defaults = $groupBackend->getOpenLdapDefaults();
$groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false!
} else { // $formData['type'] === 'msldap'
$defaults = $groupBackend->getActiveDirectoryDefaults();
$groupConfigDisabled = $userConfigDisabled = true;
}
$dnDisabled = null; // MUST BE null
if (isset($formData['user_backend']) && $formData['user_backend'] !== 'none') {
$userBackend = UserBackend::create($formData['user_backend']);
$defaults->merge(array(
'user_base_dn' => $userBackend->getBaseDn(),
'user_class' => $userBackend->getUserClass(),
'user_name_attribute' => $userBackend->getUserNameAttribute(),
'user_filter' => $userBackend->getFilter()
));
$userConfigDisabled = $dnDisabled = true;
}
$this->createGroupConfigElements($defaults, $groupConfigDisabled);
$this->createUserConfigElements($defaults, $userConfigDisabled, $dnDisabled);
}
/**
* Create and add all elements to this form required for the group configuration
*
* @param ConfigObject $defaults
* @param null|bool $disabled
*/
protected function createGroupConfigElements(ConfigObject $defaults, $disabled)
{
$this->addElement(
'text',
'group_class',
array(
'preserveDefault' => true,
'disabled' => $disabled,
'label' => $this->translate('LDAP Group Object Class'),
'description' => $this->translate('The object class used for storing groups on the LDAP server.'),
'value' => $defaults->group_class
)
);
$this->addElement(
'text',
'group_filter',
array(
'preserveDefault' => true,
'allowEmpty' => true,
'disabled' => $disabled,
'label' => $this->translate('LDAP Group Filter'),
'description' => $this->translate(
'An additional filter to use when looking up groups using the specified connection. '
. 'Leave empty to not to use any additional filter rules.'
),
'requirement' => $this->translate(
'The filter needs to be expressed as standard LDAP expression, without'
. ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
),
'validators' => array(
array(
'Callback',
false,
array(
'callback' => function ($v) {
return strpos($v, '(') !== 0;
},
'messages' => array(
'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
)
)
)
),
'value' => $defaults->group_filter
)
);
$this->addElement(
'text',
'group_name_attribute',
array(
'preserveDefault' => true,
'disabled' => $disabled,
'label' => $this->translate('LDAP Group Name Attribute'),
'description' => $this->translate(
'The attribute name used for storing a group\'s name on the LDAP server.'
),
'value' => $defaults->group_name_attribute
)
);
$this->addElement(
'text',
'base_dn',
array(
'preserveDefault' => true,
'label' => $this->translate('LDAP Group Base DN'),
'description' => $this->translate(
'The path where groups can be found on the LDAP server. Leave ' .
'empty to select all users available using the specified connection.'
),
'value' => $defaults->base_dn
)
);
}
/**
* Create and add all elements to this form required for the user configuration
*
* @param ConfigObject $defaults
* @param null|bool $disabled
* @param null|bool $dnDisabled
*/
protected function createUserConfigElements(ConfigObject $defaults, $disabled, $dnDisabled)
{
$this->addElement(
'text',
'user_class',
array(
'preserveDefault' => true,
'disabled' => $disabled,
'label' => $this->translate('LDAP User Object Class'),
'description' => $this->translate('The object class used for storing users on the LDAP server.'),
'value' => $defaults->user_class
)
);
$this->addElement(
'text',
'user_filter',
array(
'preserveDefault' => true,
'allowEmpty' => true,
'disabled' => $disabled,
'label' => $this->translate('LDAP User Filter'),
'description' => $this->translate(
'An additional filter to use when looking up users using the specified connection. '
. 'Leave empty to not to use any additional filter rules.'
),
'requirement' => $this->translate(
'The filter needs to be expressed as standard LDAP expression, without'
. ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
),
'validators' => array(
array(
'Callback',
false,
array(
'callback' => function ($v) {
return strpos($v, '(') !== 0;
},
'messages' => array(
'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
)
)
)
),
'value' => $defaults->user_filter
)
);
$this->addElement(
'text',
'user_name_attribute',
array(
'preserveDefault' => true,
'disabled' => $disabled,
'label' => $this->translate('LDAP User Name Attribute'),
'description' => $this->translate(
'The attribute name used for storing a user\'s name on the LDAP server.'
),
'value' => $defaults->user_name_attribute
)
);
$this->addElement(
'text',
'user_base_dn',
array(
'preserveDefault' => true,
'disabled' => $dnDisabled,
'label' => $this->translate('LDAP User Base DN'),
'description' => $this->translate(
'The path where users can be found on the LDAP server. Leave ' .
'empty to select all users available using the specified connection.'
),
'value' => $defaults->user_base_dn
)
);
}
/**
* Return the names of all configured LDAP resources
*
* @return array
*/
protected function getLdapResourceNames()
{
$names = array();
foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
if (in_array(strtolower($config->type), array('ldap', 'msldap'))) {
$names[] = $name;
}
}
if (empty($names)) {
Notification::error(
$this->translate('No LDAP resources available. Please configure an LDAP resource first.')
);
$this->getResponse()->redirectAndExit('config/createresource');
}
return $names;
}
/**
* Return the names of all configured LDAP user backends
*
* @param Connection $resource
*
* @return array
*/
protected function getLdapUserBackendNames(Connection $resource)
{
$names = array();
foreach (Config::app('authentication') as $name => $config) {
if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) {
$backendResource = ResourceFactory::create($config->resource);
if (
$backendResource->getHostname() === $resource->getHostname()
&& $backendResource->getPort() === $resource->getPort()
) {
$names[] = $name;
}
}
}
return $names;
}
}

View File

@ -13,6 +13,13 @@ use Icinga\Forms\ConfigForm;
*/
class UserGroupBackendForm extends ConfigForm
{
/**
* The backend to load when displaying the form for the first time
*
* @var string
*/
protected $backendToLoad;
/**
* Initialize this form
*/
@ -31,10 +38,17 @@ class UserGroupBackendForm extends ConfigForm
*/
public function getBackendForm($type)
{
if ($type === 'db') {
return new DbUserGroupBackendForm();
} else {
throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type));
switch ($type)
{
case 'db':
return new DbUserGroupBackendForm();
case 'ldap':
case 'msldap':
return new LdapUserGroupBackendForm();
default:
throw new InvalidArgumentException(
sprintf($this->translate('Invalid backend type "%s" provided'), $type)
);
}
}
@ -53,10 +67,7 @@ class UserGroupBackendForm extends ConfigForm
throw new NotFoundError('No user group backend called "%s" found', $name);
}
$data = $this->config->getSection($name)->toArray();
$data['type'] = $data['backend'];
$data['name'] = $name;
$this->populate($data);
$this->backendToLoad = $name;
return $this;
}
@ -103,13 +114,23 @@ class UserGroupBackendForm extends ConfigForm
}
$backendConfig = $this->config->getSection($name);
if (isset($data['name']) && $data['name'] !== $name) {
$this->config->removeSection($name);
$name = $data['name'];
if (isset($data['name'])) {
if ($data['name'] !== $name) {
$this->config->removeSection($name);
$name = $data['name'];
}
unset($data['name']);
}
$this->config->setSection($name, $backendConfig->merge($data));
$backendConfig->merge($data);
foreach ($backendConfig->toArray() as $k => $v) {
if ($v === null) {
unset($backendConfig->$k);
}
}
$this->config->setSection($name, $backendConfig);
return $this;
}
@ -161,7 +182,9 @@ class UserGroupBackendForm extends ConfigForm
// TODO(jom): We did not think about how to configure custom group backends yet!
$backendTypes = array(
'db' => $this->translate('Database')
'db' => $this->translate('Database'),
'ldap' => $this->translate('LDAP'),
'msldap' => $this->translate('ActiveDirectory')
);
$backendType = isset($formData['type']) ? $formData['type'] : null;
@ -191,8 +214,34 @@ class UserGroupBackendForm extends ConfigForm
)
);
$backendForm = $this->getBackendForm($backendType);
$backendForm->createElements($formData);
$this->addElements($backendForm->getElements());
$this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
}
/**
* Populate the configuration of the backend to load
*/
public function onRequest()
{
if ($this->backendToLoad) {
$data = $this->config->getSection($this->backendToLoad)->toArray();
$data['type'] = $data['backend'];
$data['name'] = $this->backendToLoad;
$this->populate($data);
}
}
/**
* Retrieve all form element values
*
* @param bool $suppressArrayNotation Ignored
*
* @return array
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['backend_form']);
unset($values['backend_form']);
return $values;
}
}

View File

@ -4,17 +4,16 @@
namespace Icinga\Authentication\User;
use DateTime;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ProgrammingError;
use Icinga\Repository\Repository;
use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression;
use Icinga\User;
class LdapUserBackend extends Repository implements UserBackendInterface
class LdapUserBackend extends LdapRepository implements UserBackendInterface
{
/**
* The base DN to use for a query
@ -65,20 +64,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
)
);
protected $groupOptions;
/**
* Normed attribute names based on known LDAP environments
*
* @var array
*/
protected $normedAttributes = array(
'uid' => 'uid',
'user' => 'user',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName'
);
/**
* Set the base DN to use for a query
*
@ -179,34 +164,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
return $this->filter;
}
public function setGroupOptions(array $options)
{
$this->groupOptions = $options;
return $this;
}
public function getGroupOptions()
{
return $this->groupOptions;
}
/**
* Return the given attribute name normed to known LDAP enviroments, if possible
*
* @param string $name
*
* @return string
*/
protected function getNormedAttribute($name)
{
$loweredName = strtolower($name);
if (array_key_exists($loweredName, $this->normedAttributes)) {
return $this->normedAttributes[$loweredName];
}
return $name;
}
/**
* Apply the given configuration to this backend
*
@ -325,37 +282,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0;
}
/**
* Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation
*
* @param string|null $value
*
* @return int
*/
protected function retrieveGeneralizedTime($value)
{
if ($value === null) {
return $value;
}
if (
($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false
) {
return $dateTime->getTimeStamp();
} else {
Logger::debug(sprintf(
'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) for user backend "%s".',
$value,
$this->getName()
));
}
}
/**
* Return whether the given shadowExpire value defines that a user is permitted to login
*
@ -413,41 +339,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
}
}
/**
* Retrieve the user groups
*
* @TODO: Subject to change, see #7343
*
* @param string $dn
*
* @return array
*/
public function getGroups($dn)
{
if (empty($this->groupOptions) || ! isset($this->groupOptions['group_base_dn'])) {
return array();
}
$result = $this->ds->select()
->setBase($this->groupOptions['group_base_dn'])
->from(
$this->groupOptions['group_class'],
array($this->groupOptions['group_attribute'])
)
->where(
$this->groupOptions['group_member_attribute'],
$dn
)
->fetchAll();
$groups = array();
foreach ($result as $group) {
$groups[] = $group->{$this->groupOptions['group_attribute']};
}
return $groups;
}
/**
* Authenticate the given user
*
@ -472,15 +363,7 @@ class LdapUserBackend extends Repository implements UserBackendInterface
return false;
}
$authenticated = $this->ds->testCredentials($userDn, $password);
if ($authenticated) {
$groups = $this->getGroups($userDn);
if ($groups !== null) {
$user->setGroups($groups);
}
}
return $authenticated;
return $this->ds->testCredentials($userDn, $password);
} catch (LdapException $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',

View File

@ -3,6 +3,7 @@
namespace Icinga\Authentication\User;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Application\Icinga;
use Icinga\Data\ConfigObject;
@ -106,8 +107,17 @@ class UserBackend
*
* @throws ConfigurationError
*/
public static function create($name, ConfigObject $backendConfig)
public static function create($name, ConfigObject $backendConfig = null)
{
if ($backendConfig === null) {
$authConfig = Config::app('authentication');
if ($authConfig->hasSection($name)) {
$backendConfig = $authConfig->getSection($name);
} else {
throw new ConfigurationError('User backend "%s" does not exist', $name);
}
}
if ($backendConfig->name !== null) {
$name = $backendConfig->name;
}
@ -165,12 +175,6 @@ class UserBackend
$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')
));
break;
case 'ldap':
$backend = new LdapUserBackend($resource);
@ -178,12 +182,6 @@ class UserBackend
$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
));
break;
}

View File

@ -0,0 +1,627 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\LdapUserBackend;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\Expression;
use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\User;
class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBackendInterface
{
/**
* The base DN to use for a user query
*
* @var string
*/
protected $userBaseDn;
/**
* The base DN to use for a group query
*
* @var string
*/
protected $groupBaseDn;
/**
* The objectClass where look for users
*
* @var string
*/
protected $userClass;
/**
* The objectClass where look for groups
*
* @var string
*/
protected $groupClass;
/**
* The attribute name where to find a user's name
*
* @var string
*/
protected $userNameAttribute;
/**
* The attribute name where to find a group's name
*
* @var string
*/
protected $groupNameAttribute;
/**
* The attribute name where to find a group's member
*
* @var string
*/
protected $groupMemberAttribute;
/**
* The custom LDAP filter to apply on a user query
*
* @var string
*/
protected $userFilter;
/**
* The custom LDAP filter to apply on a group query
*
* @var string
*/
protected $groupFilter;
/**
* The columns which are not permitted to be queried
*
* @var array
*/
protected $filterColumns = array('group', 'user');
/**
* The default sort rules to be applied on a query
*
* @var array
*/
protected $sortRules = array(
'group_name' => array(
'order' => 'asc'
)
);
/**
* Normed attribute names based on known LDAP environments
*
* @var array
*/
protected $normedAttributes = array(
'uid' => 'uid',
'gid' => 'gid',
'user' => 'user',
'group' => 'group',
'member' => 'member',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName'
);
/**
* The name of this repository
*
* @var string
*/
protected $name;
/**
* The datasource being used
*
* @var Connection
*/
protected $ds;
/**
* Create a new LDAP repository object
*
* @param Connection $ds The data source to use
*/
public function __construct($ds)
{
$this->ds = $ds;
}
/**
* Return the given attribute name normed to known LDAP enviroments, if possible
*
* @param string $name
*
* @return string
*/
protected function getNormedAttribute($name)
{
$loweredName = strtolower($name);
if (array_key_exists($loweredName, $this->normedAttributes)) {
return $this->normedAttributes[$loweredName];
}
return $name;
}
/**
* Set this repository's name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Return this repository's name
*
* In case no name has been explicitly set yet, the class name is returned.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the base DN to use for a user query
*
* @param string $baseDn
*
* @return $this
*/
public function setUserBaseDn($baseDn)
{
if (($baseDn = trim($baseDn))) {
$this->userBaseDn = $baseDn;
}
return $this;
}
/**
* Return the base DN to use for a user query
*
* @return string
*/
public function getUserBaseDn()
{
return $this->userBaseDn;
}
/**
* Set the base DN to use for a group query
*
* @param string $baseDn
*
* @return $this
*/
public function setGroupBaseDn($baseDn)
{
if (($baseDn = trim($baseDn))) {
$this->groupBaseDn = $baseDn;
}
return $this;
}
/**
* Return the base DN to use for a group query
*
* @return string
*/
public function getGroupBaseDn()
{
return $this->groupBaseDn;
}
/**
* Set the objectClass where to look for users
*
* @param string $userClass
*
* @return $this
*/
public function setUserClass($userClass)
{
$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 objectClass where to look for groups
*
* Sets also the base table name for the underlying repository.
*
* @param string $groupClass
*
* @return $this
*/
public function setGroupClass($groupClass)
{
$this->baseTable = $this->groupClass = $this->getNormedAttribute($groupClass);
return $this;
}
/**
* Return the objectClass where to look for groups
*
* @return string
*/
public function getGroupClass()
{
return $this->groupClass;
}
/**
* 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);
return $this;
}
/**
* Return the attribute name where to find a user's name
*
* @return string
*/
public function getUserNameAttribute()
{
return $this->userNameAttribute;
}
/**
* Set the attribute name where to find a group's name
*
* @param string $groupNameAttribute
*
* @return $this
*/
public function setGroupNameAttribute($groupNameAttribute)
{
$this->groupNameAttribute = $this->getNormedAttribute($groupNameAttribute);
return $this;
}
/**
* Return the attribute name where to find a group's name
*
* @return string
*/
public function getGroupNameAttribute()
{
return $this->groupNameAttribute;
}
/**
* Set the attribute name where to find a group's member
*
* @param string $groupMemberAttribute
*
* @return $this
*/
public function setGroupMemberAttribute($groupMemberAttribute)
{
$this->groupMemberAttribute = $this->getNormedAttribute($groupMemberAttribute);
return $this;
}
/**
* Return the attribute name where to find a group's member
*
* @return string
*/
public function getGroupMemberAttribute()
{
return $this->groupMemberAttribute;
}
/**
* Set the custom LDAP filter to apply on a user query
*
* @param string $filter
*
* @return $this
*/
public function setUserFilter($filter)
{
if (($filter = trim($filter))) {
$this->userFilter = $filter;
}
return $this;
}
/**
* Return the custom LDAP filter to apply on a user query
*
* @return string
*/
public function getUserFilter()
{
return $this->userFilter;
}
/**
* Set the custom LDAP filter to apply on a group query
*
* @param string $filter
*
* @return $this
*/
public function setGroupFilter($filter)
{
if (($filter = trim($filter))) {
$this->groupFilter = $filter;
}
return $this;
}
/**
* Return the custom LDAP filter to apply on a group query
*
* @return string
*/
public function getGroupFilter()
{
return $this->groupFilter;
}
/**
* 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)
{
$query = parent::select($columns);
$query->getQuery()->setBase($this->groupBaseDn);
if ($this->groupFilter) {
// TODO(jom): This should differentiate between groups and their memberships
$query->getQuery()->where(new Expression($this->groupFilter));
}
return $query;
}
/**
* Initialize this repository's query columns
*
* @return array
*
* @throws ProgrammingError In case either $this->groupNameAttribute or $this->groupClass has not been set yet
*/
protected function initializeQueryColumns()
{
if ($this->groupClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for groups first');
}
if ($this->groupNameAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first');
}
if ($this->ds->getCapabilities()->hasAdOid()) {
$createdAtAttribute = 'whenCreated';
$lastModifiedAttribute = 'whenChanged';
} else {
$createdAtAttribute = 'createTimestamp';
$lastModifiedAttribute = 'modifyTimestamp';
}
// TODO(jom): Fetching memberships does not work currently, we'll need some aggregate functionality!
$columns = array(
'group' => $this->groupNameAttribute,
'group_name' => $this->groupNameAttribute,
'user' => $this->groupMemberAttribute,
'user_name' => $this->groupMemberAttribute,
'created_at' => $createdAtAttribute,
'last_modified' => $lastModifiedAttribute
);
return array('group' => $columns, 'group_membership' => $columns);
}
/**
* Initialize this repository's conversion rules
*
* @return array
*
* @throws ProgrammingError In case $this->groupClass has not been set yet
*/
protected function initializeConversionRules()
{
if ($this->groupClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for groups first');
}
return array(
$this->groupClass => array(
'created_at' => 'generalized_time',
'last_modified' => 'generalized_time'
)
);
}
/**
* Validate that the requested table exists
*
* This will return $this->groupClass in case $table equals "group" or "group_membership".
*
* @param string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context
* (unused by the base implementation)
*
* @return string
*
* @throws ProgrammingError In case the given table does not exist
*/
public function requireTable($table, RepositoryQuery $query = null)
{
$table = parent::requireTable($table, $query);
if ($table === 'group' || $table === 'group_membership') {
$table = $this->groupClass;
}
return $table;
}
/**
* Return the groups the given user is a member of
*
* @param User $user
*
* @return array
*/
public function getMemberships(User $user)
{
$userQuery = $this->ds
->select()
->from($this->userClass)
->where($this->userNameAttribute, $user->getUsername())
->setBase($this->userBaseDn)
->setUsePagedResults(false);
if ($this->userFilter) {
$userQuery->where(new Expression($this->userFilter));
}
if (($userDn = $userQuery->fetchDn()) === null) {
return array();
}
$groupQuery = $this->ds
->select()
->from($this->groupClass, array($this->groupNameAttribute))
->where($this->groupMemberAttribute, $userDn)
->setBase($this->groupBaseDn);
if ($this->groupFilter) {
$groupQuery->where(new Expression($this->groupFilter));
}
$groups = array();
foreach ($groupQuery as $row) {
$groups[] = $row->{$this->groupNameAttribute};
}
return $groups;
}
/**
* Apply the given configuration on this backend
*
* @param ConfigObject $config
*
* @return $this
*
* @throws ConfigurationError In case a linked user backend does not exist or is invalid
*/
public function setConfig(ConfigObject $config)
{
if ($config->backend === 'ldap') {
$defaults = $this->getOpenLdapDefaults();
} elseif ($config->backend === 'msldap') {
$defaults = $this->getActiveDirectoryDefaults();
} else {
$defaults = new ConfigObject();
}
if ($config->user_backend && $config->user_backend !== 'none') {
$userBackend = UserBackend::create($config->user_backend);
if (! $userBackend instanceof LdapUserBackend) {
throw new ConfigurationError('User backend "%s" is not of type LDAP', $config->user_backend);
}
if (
$this->ds->getHostname() !== $userBackend->getDataSource()->getHostname()
|| $this->ds->getPort() !== $userBackend->getDataSource()->getPort()
) {
// TODO(jom): Elaborate whether it makes sense to link directories on different hosts
throw new ConfigurationError(
'It is required that a linked user backend refers to the '
. 'same directory as it\'s user group backend counterpart'
);
}
$defaults->merge(array(
'user_base_dn' => $userBackend->getBaseDn(),
'user_class' => $userBackend->getUserClass(),
'user_name_attribute' => $userBackend->getUserNameAttribute(),
'user_filter' => $userBackend->getFilter()
));
}
return $this
->setGroupBaseDn($config->base_dn)
->setUserBaseDn($config->get('user_base_dn', $this->getGroupBaseDn()))
->setGroupClass($config->get('group_class', $defaults->group_class))
->setUserClass($config->get('user_class', $defaults->user_class))
->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute))
->setUserNameAttribute($config->get('user_name_attribute', $defaults->user_name_attribute))
->setGroupMemberAttribute($config->get('group_member_attribute', $defaults->group_member_attribute))
->setGroupFilter($config->filter)
->setUserFilter($config->user_filter);
}
/**
* Return the configuration defaults for an OpenLDAP environment
*
* @return ConfigObject
*/
public function getOpenLdapDefaults()
{
return new ConfigObject(array(
'group_class' => 'group',
'user_class' => 'inetOrgPerson',
'group_name_attribute' => 'gid',
'user_name_attribute' => 'uid',
'group_member_attribute' => 'member'
));
}
/**
* Return the configuration defaults for an ActiveDirectory environment
*
* @return ConfigObject
*/
public function getActiveDirectoryDefaults()
{
return new ConfigObject(array(
'group_class' => 'group',
'user_class' => 'user',
'group_name_attribute' => 'sAMAccountName',
'user_name_attribute' => 'sAMAccountName',
'group_member_attribute' => 'member'
));
}
}

View File

@ -21,6 +21,8 @@ class UserGroupBackend
*/
protected static $defaultBackends = array(
'db',
'ldap',
'msldap',
//'ini'
);
@ -156,6 +158,11 @@ class UserGroupBackend
case 'ini':
$backend = new IniUserGroupBackend($resource);
break;
case 'ldap':
case 'msldap':
$backend = new LdapUserGroupBackend($resource);
$backend->setConfig($backendConfig);
break;
}
$backend->setName($name);

View File

@ -0,0 +1,66 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Icinga\Protocol\Ldap\Connection;
/**
* Abstract base class for concrete LDAP repository implementations
*
* Additionally provided features:
* <ul>
* <li>Attribute name normalization</li>
* </ul>
*/
abstract class LdapRepository extends Repository
{
/**
* The datasource being used
*
* @var Connection
*/
protected $ds;
/**
* Normed attribute names based on known LDAP environments
*
* @var array
*/
protected $normedAttributes = array(
'uid' => 'uid',
'gid' => 'gid',
'user' => 'user',
'group' => 'group',
'member' => 'member',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName'
);
/**
* Create a new LDAP repository object
*
* @param Connection $ds The data source to use
*/
public function __construct(Connection $ds)
{
parent::__construct($ds);
}
/**
* Return the given attribute name normed to known LDAP enviroments, if possible
*
* @param string $name
*
* @return string
*/
protected function getNormedAttribute($name)
{
$loweredName = strtolower($name);
if (array_key_exists($loweredName, $this->normedAttributes)) {
return $this->normedAttributes[$loweredName];
}
return $name;
}
}

View File

@ -599,6 +599,37 @@ abstract class Repository implements Selectable
return $value;
}
/**
* Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation
*
* @param string|null $value
*
* @return int
*/
protected function retrieveGeneralizedTime($value)
{
if ($value === null) {
return $value;
}
if (
($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false
) {
return $dateTime->getTimeStamp();
} else {
Logger::debug(sprintf(
'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".',
$value,
$this->getName()
));
}
}
/**
* Validate that the requested table exists
*

View File

@ -855,8 +855,19 @@ class Form extends Zend_Form
public function populate(array $defaults)
{
$this->create($defaults);
$this->preserveDefaults($this, $defaults);
return parent::populate($defaults);
}
foreach ($this->getElements() as $name => $_) {
/**
* Recurse the given form and unset all unchanged default values
*
* @param Zend_Form $form
* @param array $defaults
*/
protected function preserveDefaults(Zend_Form $form, array & $defaults)
{
foreach ($form->getElements() as $name => $_) {
if (
array_key_exists($name, $defaults)
&& array_key_exists($name . static::DEFAULT_SUFFIX, $defaults)
@ -866,7 +877,9 @@ class Form extends Zend_Form
}
}
return parent::populate($defaults);
foreach ($form->getSubForms() as $_ => $subForm) {
$this->preserveDefaults($subForm, $defaults);
}
}
/**