server/apps/user_ldap/lib/wizard.php
Arthur Schiwon 4fa39250e7 LDAP User Cleanup: Port from stable7 without further adjustements
LDAP User Cleanup

background job for user clean up

adjust user backend for clean up

register background job

remove dead code

dependency injection

make Helper non-static for proper testing

check whether it is OK to run clean up job. Do not forget to pass arguments.

use correct method to get the config from server

methods can be private, proper indirect testing is given

no automatic user deletion

make limit readable for test purposes

make method less complex

add first tests

let preferences accept limit and offset for getUsersForValue

DI via constructor does not work for background jobs

after detecting, now we have retrieving deleted users and their details

we need this method to be public for now

finalize export method, add missing getter

clean up namespaces and get rid of unnecessary files

helper is not static anymore

cleanup according to scrutinizer

add cli tool to show deleted users

uses are necessary after recent namespace change

also remove user from mappings table on deletion

add occ command to delete users

fix use statement

improve output

big fixes / improvements

PHP doc

return true in userExists early for cleaning up deleted users

bump version

control state and interval with one config.php setting, now ldapUserCleanupInterval. 0 will disable it. enabled by default.

improve doc

rename cli method to be consistent with  others

introduce ldapUserCleanupInterval in sample config

don't show last login as unix epoche start when no  login happend

less log output

consistent namespace for OfflineUser

rename GarbageCollector to DeletedUsersIndex and move it to user subdir

fix unit tests

add tests for deleteUser

more test adjustements

Conflicts:
	apps/user_ldap/ajax/clearMappings.php
	apps/user_ldap/appinfo/app.php
	apps/user_ldap/lib/access.php
	apps/user_ldap/lib/helper.php
	apps/user_ldap/tests/helper.php
	core/register_command.php
	lib/private/preferences.php
	lib/private/user.php

add ldap:check-user to check user existance on the fly

Conflicts:
	apps/user_ldap/lib/helper.php

forgotten file

PHPdoc fixes, no code change

and don't forget to adjust tests
2014-12-19 19:47:54 +01:00

1301 lines
37 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* ownCloud LDAP Wizard
*
* @author Arthur Schiwon
* @copyright 2013 Arthur Schiwon blizzz@owncloud.com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\user_ldap\lib;
class Wizard extends LDAPUtility {
static protected $l;
protected $access;
protected $cr;
protected $configuration;
protected $result;
protected $resultCache = array();
const LRESULT_PROCESSED_OK = 2;
const LRESULT_PROCESSED_INVALID = 3;
const LRESULT_PROCESSED_SKIP = 4;
const LFILTER_LOGIN = 2;
const LFILTER_USER_LIST = 3;
const LFILTER_GROUP_LIST = 4;
const LFILTER_MODE_ASSISTED = 2;
const LFILTER_MODE_RAW = 1;
const LDAP_NW_TIMEOUT = 4;
/**
* Constructor
* @param Configuration $configuration an instance of Configuration
* @param ILDAPWrapper $ldap an instance of ILDAPWrapper
*/
public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) {
parent::__construct($ldap);
$this->configuration = $configuration;
if(is_null(Wizard::$l)) {
Wizard::$l = \OC::$server->getL10N('user_ldap');
}
$this->access = $access;
$this->result = new WizardResult();
}
public function __destruct() {
if($this->result->hasChanges()) {
$this->configuration->saveConfiguration();
}
}
/**
* counts entries in the LDAP directory
* @param string $filter the LDAP search filter
* @param string $type a string being either 'users' or 'groups';
* @return int|bool
*/
public function countEntries($filter, $type) {
$reqs = array('ldapHost', 'ldapPort', 'ldapBase');
if($type === 'users') {
$reqs[] = 'ldapUserFilter';
}
if(!$this->checkRequirements($reqs)) {
throw new \Exception('Requirements not met', 400);
}
if($type === 'groups') {
$result = $this->access->countGroups($filter);
} else if($type === 'users') {
$result = $this->access->countUsers($filter);
} else {
throw new \Exception('internal error: invald object type', 500);
}
return $result;
}
public function countGroups() {
$filter = $this->configuration->ldapGroupFilter;
if(empty($filter)) {
$output = self::$l->n('%s group found', '%s groups found', 0, array(0));
$this->result->addChange('ldap_group_count', $output);
return $this->result;
}
try {
$groupsTotal = $this->countEntries($filter, 'groups');
} catch (\Exception $e) {
//400 can be ignored, 500 is forwarded
if($e->getCode() === 500) {
throw $e;
}
return false;
}
$groupsTotal = ($groupsTotal !== false) ? $groupsTotal : 0;
$output = self::$l->n('%s group found', '%s groups found', $groupsTotal, $groupsTotal);
$this->result->addChange('ldap_group_count', $output);
return $this->result;
}
/**
* @return WizardResult
* @throws \Exception
*/
public function countUsers() {
$filter = $this->access->getFilterForUserCount();
$usersTotal = $this->countEntries($filter, 'users');
$usersTotal = ($usersTotal !== false) ? $usersTotal : 0;
$output = self::$l->n('%s user found', '%s users found', $usersTotal, $usersTotal);
$this->result->addChange('ldap_user_count', $output);
return $this->result;
}
/**
* counts users with a specified attribute
* @param string $attr
* @param bool $existsCheck
* @return int|bool
*/
public function countUsersWithAttribute($attr, $existsCheck = false) {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
'ldapUserFilter',
))) {
return false;
}
$filter = $this->access->combineFilterWithAnd(array(
$this->configuration->ldapUserFilter,
$attr . '=*'
));
$limit = ($existsCheck === false) ? null : 1;
return $this->access->countUsers($filter, array('dn'), $limit);
}
/**
* detects the display name attribute. If a setting is already present that
* returns at least one hit, the detection will be canceled.
* @return WizardResult|bool
* @throws \Exception
*/
public function detectUserDisplayNameAttribute() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
'ldapUserFilter',
))) {
return false;
}
$attr = $this->configuration->ldapUserDisplayName;
if($attr !== 'displayName' && !empty($attr)) {
// most likely not the default value with upper case N,
// verify it still produces a result
$count = intval($this->countUsersWithAttribute($attr, true));
if($count > 0) {
//no change, but we sent it back to make sure the user interface
//is still correct, even if the ajax call was cancelled inbetween
$this->result->addChange('ldap_display_name', $attr);
return $this->result;
}
}
// first attribute that has at least one result wins
$displayNameAttrs = array('displayname', 'cn');
foreach ($displayNameAttrs as $attr) {
$count = intval($this->countUsersWithAttribute($attr, true));
if($count > 0) {
$this->applyFind('ldap_display_name', $attr);
return $this->result;
}
};
throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced ldap settings.'));
}
/**
* detects the most often used email attribute for users applying to the
* user list filter. If a setting is already present that returns at least
* one hit, the detection will be canceled.
* @return WizardResult|bool
*/
public function detectEmailAttribute() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
'ldapUserFilter',
))) {
return false;
}
$attr = $this->configuration->ldapEmailAttribute;
if(!empty($attr)) {
$count = intval($this->countUsersWithAttribute($attr, true));
if($count > 0) {
return false;
}
$writeLog = true;
} else {
$writeLog = false;
}
$emailAttributes = array('mail', 'mailPrimaryAddress');
$winner = '';
$maxUsers = 0;
foreach($emailAttributes as $attr) {
$count = $this->countUsersWithAttribute($attr);
if($count > $maxUsers) {
$maxUsers = $count;
$winner = $attr;
}
}
if($winner !== '') {
$this->applyFind('ldap_email_attr', $winner);
if($writeLog) {
\OCP\Util::writeLog('user_ldap', 'The mail attribute has ' .
'automatically been reset, because the original value ' .
'did not return any results.', \OCP\Util::INFO);
}
}
return $this->result;
}
/**
* @return WizardResult
* @throws \Exception
*/
public function determineAttributes() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
'ldapUserFilter',
))) {
return false;
}
$attributes = $this->getUserAttributes();
natcasesort($attributes);
$attributes = array_values($attributes);
$this->result->addOptions('ldap_loginfilter_attributes', $attributes);
$selected = $this->configuration->ldapLoginFilterAttributes;
if(is_array($selected) && !empty($selected)) {
$this->result->addChange('ldap_loginfilter_attributes', $selected);
}
return $this->result;
}
/**
* return the state of the Group Filter Mode
* @return WizardResult
*/
public function getGroupFilterMode() {
$this->getFilterMode('ldapGroupFilterMode');
return $this->result;
}
/**
* return the state of the Login Filter Mode
* @return WizardResult
*/
public function getLoginFilterMode() {
$this->getFilterMode('ldapLoginFilterMode');
return $this->result;
}
/**
* return the state of the User Filter Mode
* @return WizardResult
*/
public function getUserFilterMode() {
$this->getFilterMode('ldapUserFilterMode');
return $this->result;
}
/**
* return the state of the mode of the specified filter
* @param string $confKey contains the access key of the Configuration
*/
private function getFilterMode($confKey) {
$mode = $this->configuration->$confKey;
if(is_null($mode)) {
$mode = $this->LFILTER_MODE_ASSISTED;
}
$this->result->addChange($confKey, $mode);
}
/**
* detects the available LDAP attributes
* @return array The instance's WizardResult instance
* @throws \Exception
*/
private function getUserAttributes() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
'ldapUserFilter',
))) {
return false;
}
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
$base = $this->configuration->ldapBase[0];
$filter = $this->configuration->ldapUserFilter;
$rr = $this->ldap->search($cr, $base, $filter, array(), 1, 1);
if(!$this->ldap->isResource($rr)) {
return false;
}
$er = $this->ldap->firstEntry($cr, $rr);
$attributes = $this->ldap->getAttributes($cr, $er);
$pureAttributes = array();
for($i = 0; $i < $attributes['count']; $i++) {
$pureAttributes[] = $attributes[$i];
}
return $pureAttributes;
}
/**
* detects the available LDAP groups
* @return WizardResult the instance's WizardResult instance
*/
public function determineGroupsForGroups() {
return $this->determineGroups('ldap_groupfilter_groups',
'ldapGroupFilterGroups',
false);
}
/**
* detects the available LDAP groups
* @return WizardResult the instance's WizardResult instance
*/
public function determineGroupsForUsers() {
return $this->determineGroups('ldap_userfilter_groups',
'ldapUserFilterGroups');
}
/**
* detects the available LDAP groups
* @param string $dbKey
* @param string $confKey
* @param bool $testMemberOf
* @return WizardResult the instance's WizardResult instance
* @throws \Exception
*/
private function determineGroups($dbKey, $confKey, $testMemberOf = true) {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
))) {
return false;
}
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
$groups = $this->fetchGroups($dbKey, $confKey);
if($testMemberOf) {
$this->configuration->hasMemberOfFilterSupport = $this->testMemberOf($groups);
$this->result->markChange();
if(!$this->configuration->hasMemberOfFilterSupport) {
throw new \Exception('memberOf is not supported by the server');
}
}
return $this->result;
}
/**
* fetches all groups from LDAP
* @param string $dbKey
* @param string $confKey
* @return array $groupEntries
*/
public function fetchGroups($dbKey, $confKey) {
$obclasses = array('posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames');
$filterParts = array();
foreach($obclasses as $obclass) {
$filterParts[] = 'objectclass='.$obclass;
}
//we filter for everything
//- that looks like a group and
//- has the group display name set
$filter = $this->access->combineFilterWithOr($filterParts);
$filter = $this->access->combineFilterWithAnd(array($filter, 'cn=*'));
$groupNames = array();
$groupEntries = array();
$limit = 400;
$offset = 0;
do {
// we need to request dn additionally here, otherwise memberOf
// detection will fail later
$result = $this->access->searchGroups($filter, array('cn', 'dn'), $limit, $offset);
foreach($result as $item) {
$groupNames[] = $item['cn'];
$groupEntries[] = $item;
}
$offset += $limit;
} while (count($groupNames) > 0 && count($groupNames) % $limit === 0);
if(count($groupNames) > 0) {
natsort($groupNames);
$this->result->addOptions($dbKey, array_values($groupNames));
} else {
throw new \Exception(self::$l->t('Could not find the desired feature'));
}
$setFeatures = $this->configuration->$confKey;
if(is_array($setFeatures) && !empty($setFeatures)) {
//something is already configured? pre-select it.
$this->result->addChange($dbKey, $setFeatures);
}
return $groupEntries;
}
public function determineGroupMemberAssoc() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapGroupFilter',
))) {
return false;
}
$attribute = $this->detectGroupMemberAssoc();
if($attribute === false) {
return false;
}
$this->configuration->setConfiguration(array('ldapGroupMemberAssocAttr' => $attribute));
//so it will be saved on destruct
$this->result->markChange();
return $this->result;
}
/**
* Detects the available object classes
* @return WizardResult the instance's WizardResult instance
* @throws \Exception
*/
public function determineGroupObjectClasses() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
))) {
return false;
}
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
$obclasses = array('group', 'posixGroup', '*');
$this->determineFeature($obclasses,
'objectclass',
'ldap_groupfilter_objectclass',
'ldapGroupFilterObjectclass',
false);
return $this->result;
}
/**
* detects the available object classes
* @return WizardResult
* @throws \Exception
*/
public function determineUserObjectClasses() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
))) {
return false;
}
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
$obclasses = array('inetOrgPerson', 'person', 'organizationalPerson',
'user', 'posixAccount', '*');
$filter = $this->configuration->ldapUserFilter;
//if filter is empty, it is probably the first time the wizard is called
//then, apply suggestions.
$this->determineFeature($obclasses,
'objectclass',
'ldap_userfilter_objectclass',
'ldapUserFilterObjectclass',
empty($filter));
return $this->result;
}
/**
* @return WizardResult
* @throws \Exception
*/
public function getGroupFilter() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
))) {
return false;
}
//make sure the use display name is set
$displayName = $this->configuration->ldapGroupDisplayName;
if(empty($displayName)) {
$d = $this->configuration->getDefaults();
$this->applyFind('ldap_group_display_name',
$d['ldap_group_display_name']);
}
$filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
$this->applyFind('ldap_group_filter', $filter);
return $this->result;
}
/**
* @return WizardResult
* @throws \Exception
*/
public function getUserListFilter() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
))) {
return false;
}
//make sure the use display name is set
$displayName = $this->configuration->ldapUserDisplayName;
if(empty($displayName)) {
$d = $this->configuration->getDefaults();
$this->applyFind('ldap_display_name', $d['ldap_display_name']);
}
$filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
if(!$filter) {
throw new \Exception('Cannot create filter');
}
$this->applyFind('ldap_userlist_filter', $filter);
return $this->result;
}
/**
* @return bool|WizardResult
* @throws \Exception
*/
public function getUserLoginFilter() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
'ldapBase',
'ldapUserFilter',
))) {
return false;
}
$filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
if(!$filter) {
throw new \Exception('Cannot create filter');
}
$this->applyFind('ldap_login_filter', $filter);
return $this->result;
}
/**
* Tries to determine the port, requires given Host, User DN and Password
* @return WizardResult|false WizardResult on success, false otherwise
* @throws \Exception
*/
public function guessPortAndTLS() {
if(!$this->checkRequirements(array('ldapHost',
))) {
return false;
}
$this->checkHost();
$portSettings = $this->getPortSettingsToTry();
if(!is_array($portSettings)) {
throw new \Exception(print_r($portSettings, true));
}
//proceed from the best configuration and return on first success
foreach($portSettings as $setting) {
$p = $setting['port'];
$t = $setting['tls'];
\OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, \OCP\Util::DEBUG);
//connectAndBind may throw Exception, it needs to be catched by the
//callee of this method
if($this->connectAndBind($p, $t) === true) {
$config = array('ldapPort' => $p,
'ldapTLS' => intval($t)
);
$this->configuration->setConfiguration($config);
\OCP\Util::writeLog('user_ldap', 'Wiz: detected Port '. $p, \OCP\Util::DEBUG);
$this->result->addChange('ldap_port', $p);
return $this->result;
}
}
//custom port, undetected (we do not brute force)
return false;
}
/**
* tries to determine a base dn from User DN or LDAP Host
* @return WizardResult|false WizardResult on success, false otherwise
*/
public function guessBaseDN() {
if(!$this->checkRequirements(array('ldapHost',
'ldapPort',
))) {
return false;
}
//check whether a DN is given in the agent name (99.9% of all cases)
$base = null;
$i = stripos($this->configuration->ldapAgentName, 'dc=');
if($i !== false) {
$base = substr($this->configuration->ldapAgentName, $i);
if($this->testBaseDN($base)) {
$this->applyFind('ldap_base', $base);
return $this->result;
}
}
//this did not help :(
//Let's see whether we can parse the Host URL and convert the domain to
//a base DN
$helper = new Helper();
$domain = $helper->getDomainFromURL($this->configuration->ldapHost);
if(!$domain) {
return false;
}
$dparts = explode('.', $domain);
$base2 = implode('dc=', $dparts);
if($base !== $base2 && $this->testBaseDN($base2)) {
$this->applyFind('ldap_base', $base2);
return $this->result;
}
return false;
}
/**
* sets the found value for the configuration key in the WizardResult
* as well as in the Configuration instance
* @param string $key the configuration key
* @param string $value the (detected) value
*
*/
private function applyFind($key, $value) {
$this->result->addChange($key, $value);
$this->configuration->setConfiguration(array($key => $value));
}
/**
* Checks, whether a port was entered in the Host configuration
* field. In this case the port will be stripped off, but also stored as
* setting.
*/
private function checkHost() {
$host = $this->configuration->ldapHost;
$hostInfo = parse_url($host);
//removes Port from Host
if(is_array($hostInfo) && isset($hostInfo['port'])) {
$port = $hostInfo['port'];
$host = str_replace(':'.$port, '', $host);
$this->applyFind('ldap_host', $host);
$this->applyFind('ldap_port', $port);
}
}
/**
* tries to detect the group member association attribute which is
* one of 'uniqueMember', 'memberUid', 'member'
* @return string|false, string with the attribute name, false on error
* @throws \Exception
*/
private function detectGroupMemberAssoc() {
$possibleAttrs = array('uniqueMember', 'memberUid', 'member', 'unfugasdfasdfdfa');
$filter = $this->configuration->ldapGroupFilter;
if(empty($filter)) {
return false;
}
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
$base = $this->configuration->ldapBase[0];
$rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs);
if(!$this->ldap->isResource($rr)) {
return false;
}
$er = $this->ldap->firstEntry($cr, $rr);
while(is_resource($er)) {
$this->ldap->getDN($cr, $er);
$attrs = $this->ldap->getAttributes($cr, $er);
$result = array();
$possibleAttrsCount = count($possibleAttrs);
for($i = 0; $i < $possibleAttrsCount; $i++) {
if(isset($attrs[$possibleAttrs[$i]])) {
$result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
}
}
if(!empty($result)) {
natsort($result);
return key($result);
}
$er = $this->ldap->nextEntry($cr, $er);
}
return false;
}
/**
* Checks whether for a given BaseDN results will be returned
* @param string $base the BaseDN to test
* @return bool true on success, false otherwise
* @throws \Exception
*/
private function testBaseDN($base) {
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
//base is there, let's validate it. If we search for anything, we should
//get a result set > 0 on a proper base
$rr = $this->ldap->search($cr, $base, 'objectClass=*', array('dn'), 0, 1);
if(!$this->ldap->isResource($rr)) {
$errorNo = $this->ldap->errno($cr);
$errorMsg = $this->ldap->error($cr);
\OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base.
' Error '.$errorNo.': '.$errorMsg, \OCP\Util::INFO);
return false;
}
$entries = $this->ldap->countEntries($cr, $rr);
return ($entries !== false) && ($entries > 0);
}
/**
* Checks whether the server supports memberOf in LDAP Filter.
* Requires that groups are determined, thus internally called from within
* determineGroups()
* @param array $groups
* @return bool true if it does, false otherwise
* @throws \Exception
*/
private function testMemberOf($groups) {
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
if(!is_array($this->configuration->ldapBase)
|| !isset($this->configuration->ldapBase[0])) {
return false;
}
$base = $this->configuration->ldapBase[0];
$filterPrefix = '(&(objectclass=*)(memberOf=';
$filterSuffix = '))';
foreach($groups as $groupProperties) {
if(!isset($groupProperties['cn'])) {
//assuming only groups have their cn cached :)
continue;
}
$filter = strtolower($filterPrefix . $groupProperties['dn'] . $filterSuffix);
$rr = $this->ldap->search($cr, $base, $filter, array('dn'));
if(!$this->ldap->isResource($rr)) {
continue;
}
$entries = $this->ldap->countEntries($cr, $rr);
//we do not know which groups are empty, so test any and return
//success on the first match that returns at least one user
if(($entries !== false) && ($entries > 0)) {
return true;
}
}
return false;
}
/**
* creates an LDAP Filter from given configuration
* @param integer $filterType int, for which use case the filter shall be created
* can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
* self::LFILTER_GROUP_LIST
* @return string|false string with the filter on success, false otherwise
* @throws \Exception
*/
private function composeLdapFilter($filterType) {
$filter = '';
$parts = 0;
switch ($filterType) {
case self::LFILTER_USER_LIST:
$objcs = $this->configuration->ldapUserFilterObjectclass;
//glue objectclasses
if(is_array($objcs) && count($objcs) > 0) {
$filter .= '(|';
foreach($objcs as $objc) {
$filter .= '(objectclass=' . $objc . ')';
}
$filter .= ')';
$parts++;
}
//glue group memberships
if($this->configuration->hasMemberOfFilterSupport) {
$cns = $this->configuration->ldapUserFilterGroups;
if(is_array($cns) && count($cns) > 0) {
$filter .= '(|';
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
$base = $this->configuration->ldapBase[0];
foreach($cns as $cn) {
$rr = $this->ldap->search($cr, $base, 'cn=' . $cn, array('dn'));
if(!$this->ldap->isResource($rr)) {
continue;
}
$er = $this->ldap->firstEntry($cr, $rr);
$dn = $this->ldap->getDN($cr, $er);
$filter .= '(memberof=' . $dn . ')';
}
$filter .= ')';
}
$parts++;
}
//wrap parts in AND condition
if($parts > 1) {
$filter = '(&' . $filter . ')';
}
if(empty($filter)) {
$filter = '(objectclass=*)';
}
break;
case self::LFILTER_GROUP_LIST:
$objcs = $this->configuration->ldapGroupFilterObjectclass;
//glue objectclasses
if(is_array($objcs) && count($objcs) > 0) {
$filter .= '(|';
foreach($objcs as $objc) {
$filter .= '(objectclass=' . $objc . ')';
}
$filter .= ')';
$parts++;
}
//glue group memberships
$cns = $this->configuration->ldapGroupFilterGroups;
if(is_array($cns) && count($cns) > 0) {
$filter .= '(|';
$base = $this->configuration->ldapBase[0];
foreach($cns as $cn) {
$filter .= '(cn=' . $cn . ')';
}
$filter .= ')';
}
$parts++;
//wrap parts in AND condition
if($parts > 1) {
$filter = '(&' . $filter . ')';
}
break;
case self::LFILTER_LOGIN:
$ulf = $this->configuration->ldapUserFilter;
$loginpart = '=%uid';
$filterUsername = '';
$userAttributes = $this->getUserAttributes();
$userAttributes = array_change_key_case(array_flip($userAttributes));
$parts = 0;
$x = $this->configuration->ldapLoginFilterUsername;
if($this->configuration->ldapLoginFilterUsername === '1') {
$attr = '';
if(isset($userAttributes['uid'])) {
$attr = 'uid';
} else if(isset($userAttributes['samaccountname'])) {
$attr = 'samaccountname';
} else if(isset($userAttributes['cn'])) {
//fallback
$attr = 'cn';
}
if(!empty($attr)) {
$filterUsername = '(' . $attr . $loginpart . ')';
$parts++;
}
}
$filterEmail = '';
if($this->configuration->ldapLoginFilterEmail === '1') {
$filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
$parts++;
}
$filterAttributes = '';
$attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
if(is_array($attrsToFilter) && count($attrsToFilter) > 0) {
$filterAttributes = '(|';
foreach($attrsToFilter as $attribute) {
$filterAttributes .= '(' . $attribute . $loginpart . ')';
}
$filterAttributes .= ')';
$parts++;
}
$filterLogin = '';
if($parts > 1) {
$filterLogin = '(|';
}
$filterLogin .= $filterUsername;
$filterLogin .= $filterEmail;
$filterLogin .= $filterAttributes;
if($parts > 1) {
$filterLogin .= ')';
}
$filter = '(&'.$ulf.$filterLogin.')';
break;
}
\OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, \OCP\Util::DEBUG);
return $filter;
}
/**
* Connects and Binds to an LDAP Server
* @param int $port the port to connect with
* @param bool $tls whether startTLS is to be used
* @param bool $ncc
* @return bool
* @throws \Exception
*/
private function connectAndBind($port = 389, $tls = false, $ncc = false) {
if($ncc) {
//No certificate check
//FIXME: undo afterwards
putenv('LDAPTLS_REQCERT=never');
}
//connect, does not really trigger any server communication
\OCP\Util::writeLog('user_ldap', 'Wiz: Checking Host Info ', \OCP\Util::DEBUG);
$host = $this->configuration->ldapHost;
$hostInfo = parse_url($host);
if(!$hostInfo) {
throw new \Exception($this->l->t('Invalid Host'));
}
if(isset($hostInfo['scheme'])) {
if(isset($hostInfo['port'])) {
//problem
} else {
$host .= ':' . $port;
}
}
\OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', \OCP\Util::DEBUG);
$cr = $this->ldap->connect($host, $port);
if(!is_resource($cr)) {
throw new \Exception($this->l->t('Invalid Host'));
}
\OCP\Util::writeLog('user_ldap', 'Wiz: Setting LDAP Options ', \OCP\Util::DEBUG);
//set LDAP options
$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
if($tls) {
$isTlsWorking = @$this->ldap->startTls($cr);
if(!$isTlsWorking) {
return false;
}
}
\OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', \OCP\Util::DEBUG);
//interesting part: do the bind!
$login = $this->ldap->bind($cr,
$this->configuration->ldapAgentName,
$this->configuration->ldapAgentPassword);
if($login === true) {
$this->ldap->unbind($cr);
if($ncc) {
throw new \Exception('Certificate cannot be validated.');
}
\OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . intval($tls), \OCP\Util::DEBUG);
return true;
}
$errNo = $this->ldap->errno($cr);
$error = ldap_error($cr);
$this->ldap->unbind($cr);
if($errNo === -1 || ($errNo === 2 && $ncc)) {
//host, port or TLS wrong
return false;
} else if ($errNo === 2) {
return $this->connectAndBind($port, $tls, true);
}
throw new \Exception($error);
}
/**
* checks whether a valid combination of agent and password has been
* provided (either two values or nothing for anonymous connect)
* @return bool, true if everything is fine, false otherwise
*/
private function checkAgentRequirements() {
$agent = $this->configuration->ldapAgentName;
$pwd = $this->configuration->ldapAgentPassword;
return ( (!empty($agent) && !empty($pwd))
|| (empty($agent) && empty($pwd)));
}
/**
* @param array $reqs
* @return bool
*/
private function checkRequirements($reqs) {
$this->checkAgentRequirements();
foreach($reqs as $option) {
$value = $this->configuration->$option;
if(empty($value)) {
return false;
}
}
return true;
}
/**
* does a cumulativeSearch on LDAP to get different values of a
* specified attribute
* @param string[] $filters array, the filters that shall be used in the search
* @param string $attr the attribute of which a list of values shall be returned
* @param int $dnReadLimit the amount of how many DNs should be analyzed.
* The lower, the faster
* @param string $maxF string. if not null, this variable will have the filter that
* yields most result entries
* @return array|false an array with the values on success, false otherwise
*/
public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) {
$dnRead = array();
$foundItems = array();
$maxEntries = 0;
if(!is_array($this->configuration->ldapBase)
|| !isset($this->configuration->ldapBase[0])) {
return false;
}
$base = $this->configuration->ldapBase[0];
$cr = $this->getConnection();
if(!$this->ldap->isResource($cr)) {
return false;
}
$lastFilter = null;
if(isset($filters[count($filters)-1])) {
$lastFilter = $filters[count($filters)-1];
}
foreach($filters as $filter) {
if($lastFilter === $filter && count($foundItems) > 0) {
//skip when the filter is a wildcard and results were found
continue;
}
$rr = $this->ldap->search($cr, $base, $filter, array($attr));
if(!$this->ldap->isResource($rr)) {
continue;
}
$entries = $this->ldap->countEntries($cr, $rr);
$getEntryFunc = 'firstEntry';
if(($entries !== false) && ($entries > 0)) {
if(!is_null($maxF) && $entries > $maxEntries) {
$maxEntries = $entries;
$maxF = $filter;
}
$dnReadCount = 0;
do {
$entry = $this->ldap->$getEntryFunc($cr, $rr);
$getEntryFunc = 'nextEntry';
if(!$this->ldap->isResource($entry)) {
continue 2;
}
$rr = $entry; //will be expected by nextEntry next round
$attributes = $this->ldap->getAttributes($cr, $entry);
$dn = $this->ldap->getDN($cr, $entry);
if($dn === false || in_array($dn, $dnRead)) {
continue;
}
$newItems = array();
$state = $this->getAttributeValuesFromEntry($attributes,
$attr,
$newItems);
$dnReadCount++;
$foundItems = array_merge($foundItems, $newItems);
$this->resultCache[$dn][$attr] = $newItems;
$dnRead[] = $dn;
} while(($state === self::LRESULT_PROCESSED_SKIP
|| $this->ldap->isResource($entry))
&& ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
}
}
return array_unique($foundItems);
}
/**
* determines if and which $attr are available on the LDAP server
* @param string[] $objectclasses the objectclasses to use as search filter
* @param string $attr the attribute to look for
* @param string $dbkey the dbkey of the setting the feature is connected to
* @param string $confkey the confkey counterpart for the $dbkey as used in the
* Configuration class
* @param bool $po whether the objectClass with most result entries
* shall be pre-selected via the result
* @return array, list of found items.
* @throws \Exception
*/
private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
$cr = $this->getConnection();
if(!$cr) {
throw new \Exception('Could not connect to LDAP');
}
$p = 'objectclass=';
foreach($objectclasses as $key => $value) {
$objectclasses[$key] = $p.$value;
}
$maxEntryObjC = '';
//how deep to dig?
//When looking for objectclasses, testing few entries is sufficient,
$dig = 3;
$availableFeatures =
$this->cumulativeSearchOnAttribute($objectclasses, $attr,
$dig, $maxEntryObjC);
if(is_array($availableFeatures)
&& count($availableFeatures) > 0) {
natcasesort($availableFeatures);
//natcasesort keeps indices, but we must get rid of them for proper
//sorting in the web UI. Therefore: array_values
$this->result->addOptions($dbkey, array_values($availableFeatures));
} else {
throw new \Exception(self::$l->t('Could not find the desired feature'));
}
$setFeatures = $this->configuration->$confkey;
if(is_array($setFeatures) && !empty($setFeatures)) {
//something is already configured? pre-select it.
$this->result->addChange($dbkey, $setFeatures);
} else if($po && !empty($maxEntryObjC)) {
//pre-select objectclass with most result entries
$maxEntryObjC = str_replace($p, '', $maxEntryObjC);
$this->applyFind($dbkey, $maxEntryObjC);
$this->result->addChange($dbkey, $maxEntryObjC);
}
return $availableFeatures;
}
/**
* appends a list of values fr
* @param resource $result the return value from ldap_get_attributes
* @param string $attribute the attribute values to look for
* @param array &$known new values will be appended here
* @return int, state on of the class constants LRESULT_PROCESSED_OK,
* LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
*/
private function getAttributeValuesFromEntry($result, $attribute, &$known) {
if(!is_array($result)
|| !isset($result['count'])
|| !$result['count'] > 0) {
return self::LRESULT_PROCESSED_INVALID;
}
// strtolower on all keys for proper comparison
$result = \OCP\Util::mb_array_change_key_case($result);
$attribute = strtolower($attribute);
if(isset($result[$attribute])) {
foreach($result[$attribute] as $key => $val) {
if($key === 'count') {
continue;
}
if(!in_array($val, $known)) {
$known[] = $val;
}
}
return self::LRESULT_PROCESSED_OK;
} else {
return self::LRESULT_PROCESSED_SKIP;
}
}
/**
* @return bool|mixed
*/
private function getConnection() {
if(!is_null($this->cr)) {
return $this->cr;
}
$cr = $this->ldap->connect(
$this->configuration->ldapHost.':'.$this->configuration->ldapPort,
$this->configuration->ldapPort);
$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
if($this->configuration->ldapTLS === 1) {
$this->ldap->startTls($cr);
}
$lo = @$this->ldap->bind($cr,
$this->configuration->ldapAgentName,
$this->configuration->ldapAgentPassword);
if($lo === true) {
$this->$cr = $cr;
return $cr;
}
return false;
}
/**
* @return array
*/
private function getDefaultLdapPortSettings() {
static $settings = array(
array('port' => 7636, 'tls' => false),
array('port' => 636, 'tls' => false),
array('port' => 7389, 'tls' => true),
array('port' => 389, 'tls' => true),
array('port' => 7389, 'tls' => false),
array('port' => 389, 'tls' => false),
);
return $settings;
}
/**
* @return array
*/
private function getPortSettingsToTry() {
//389 ← LDAP / Unencrypted or StartTLS
//636 ← LDAPS / SSL
//7xxx ← UCS. need to be checked first, because both ports may be open
$host = $this->configuration->ldapHost;
$port = intval($this->configuration->ldapPort);
$portSettings = array();
//In case the port is already provided, we will check this first
if($port > 0) {
$hostInfo = parse_url($host);
if(!(is_array($hostInfo)
&& isset($hostInfo['scheme'])
&& stripos($hostInfo['scheme'], 'ldaps') !== false)) {
$portSettings[] = array('port' => $port, 'tls' => true);
}
$portSettings[] =array('port' => $port, 'tls' => false);
}
//default ports
$portSettings = array_merge($portSettings,
$this->getDefaultLdapPortSettings());
return $portSettings;
}
}