icingaweb2/library/Icinga/Authentication/User/LdapUserBackend.php

472 lines
12 KiB
PHP
Raw Normal View History

<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
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\RepositoryQuery;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression;
use Icinga\User;
class LdapUserBackend extends Repository implements UserBackendInterface
{
/**
* The base DN to use for a query
*
* @var string
*/
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;
/**
* The custom LDAP filter to apply on search queries
*
* @var string
*/
protected $filter;
/**
* The columns which are not permitted to be queried
*
* @var array
*/
protected $filterColumns = array('user');
/**
* The default sort rules to be applied on a query
*
* @var array
*/
protected $sortRules = array(
'user_name' => array(
'columns' => array(
'is_active desc',
'user_name'
)
)
);
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
*
* @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);
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;
}
/**
* 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;
}
2014-10-14 14:37:21 +02:00
/**
* Apply the given configuration to this backend
*
* @param ConfigObject $config
*
* @return $this
2014-10-14 14:37:21 +02:00
*/
public function setConfig(ConfigObject $config)
2014-10-14 14:37:21 +02:00
{
return $this
->setBaseDn($config->base_dn)
->setUserClass($config->user_class)
->setUserNameAttribute($config->user_name_attribute)
->setFilter($config->filter);
}
/**
* 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->baseDn);
if ($this->filter) {
$query->getQuery()->where(new Expression($this->filter));
}
return $query;
2014-10-14 14:37:21 +02:00
}
/**
* Initialize this repository's query columns
*
* @return array
*
* @throws ProgrammingError In case either $this->userNameAttribute or $this->userClass has not been set yet
*/
protected function initializeQueryColumns()
{
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');
}
if ($this->ds->getCapabilities()->hasAdOid()) {
$isActiveAttribute = 'userAccountControl';
$createdAtAttribute = 'whenCreated';
$lastModifiedAttribute = 'whenChanged';
} else {
$isActiveAttribute = 'unknown';
$createdAtAttribute = 'unknown';
$lastModifiedAttribute = 'unknown';
}
return array(
$this->userClass => array(
'user' => $this->userNameAttribute,
'user_name' => $this->userNameAttribute,
'is_active' => $isActiveAttribute,
'created_at' => $createdAtAttribute,
'last_modified' => $lastModifiedAttribute
)
);
}
/**
* Initialize this repository's conversion rules
*
* @return array
*
* @throws ProgrammingError In case $this->userClass has not been set yet
*/
protected function initializeConversionRules()
{
if ($this->userClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for users first');
}
if ($this->ds->getCapabilities()->hasAdOid()) {
$stateConverter = 'user_account_control';
$timeConverter = 'generalized_time';
} else {
$timeConverter = null;
$stateConverter = null;
}
return array(
$this->userClass => array(
'is_active' => $stateConverter,
'created_at' => $timeConverter,
'last_modified' => $timeConverter
)
);
}
/**
* Return whether the given userAccountControl value defines that a user is permitted to login
*
* @param string|null $value
*
* @return bool
*/
protected function retrieveUserAccountControl($value)
{
if ($value === null) {
return $value;
}
$ADS_UF_ACCOUNTDISABLE = 2;
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
) {
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()
));
}
}
/**
* Probe the backend to test if authentication is possible
*
* 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>
* <li>The specified userClass has the property specified by userNameAttribute</li>
* </ul>
*
* @throws AuthenticationException When authentication is not possible
*/
public function assertAuthenticationPossible()
{
try {
$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)',
$this->userClass,
$this->baseDn ?: $this->ds->getDn(),
$this->filter ?: 'None'
2014-07-03 16:20:45 +02:00
);
}
if (! isset($result->user_name)) {
throw new AuthenticationException(
'UserNameAttribute "%s" not existing in objectClass "%s"',
$this->userNameAttribute,
$this->userClass
2014-07-03 16:20:45 +02:00
);
}
}
/**
* Retrieve the user groups
*
2014-10-06 13:35:17 +02:00
* @TODO: Subject to change, see #7343
*
* @param string $dn
*
* @return array
*/
public function getGroups($dn)
{
2014-10-06 13:35:17 +02:00
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
*
* @param User $user
* @param string $password
*
* @return bool True on success, false on failure
*
* @throws AuthenticationException In case authentication is not possible due to an error
*/
public function authenticate(User $user, $password)
{
2014-03-28 14:45:03 +01:00
try {
$userDn = $this
->select()
->where('user_name', str_replace('*', '', $user->getUsername()))
->getQuery()
->setUsePagedResults(false)
->fetchDn();
if ($userDn === null) {
return false;
}
$authenticated = $this->ds->testCredentials($userDn, $password);
if ($authenticated) {
$groups = $this->getGroups($userDn);
if ($groups !== null) {
$user->setGroups($groups);
}
}
return $authenticated;
} catch (LdapException $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
$user->getUsername(),
$this->getName(),
$e
2014-03-28 14:45:03 +01:00
);
}
}
}