tikiwiki/patch/ldap.php
2023-11-20 20:52:04 +00:00

582 lines
20 KiB
PHP

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
/*
* Class that adds LDAP Authentication to Tiki and aids Tiki to get User/Group Information
* from a LDAP directory
*/
use Laminas\Ldap\Filter;
use Laminas\Ldap\Ldap;
use Laminas\Ldap\Exception\LdapException;
use Laminas\Ldap\Collection\DefaultIterator as LdapCollectionIterator;
use Tiki\Lib\Logs\LogsLib;
class TikiLdapLib
{
public $groups;
// var to hold a established connection
/** @var Ldap */
protected $ldaplink = null;
// var for ldap configuration parameters
protected $options = [
'host' => 'localhost',
'port' => null,
'version' => 3,
'starttls' => false,
'useSsl' => false,
'baseDn' => '',
'filter' => '(objectClass=*)',
'scope' => 'sub',
'bind_type' => 'default',
'username' => '',
'password' => '',
'userdn' => '',
'useroc' => 'inetOrgPerson',
'userattr' => 'cn',
'fullnameattr' => '',
'emailattr' => 'mail',
'countryattr' => '',
'groupdn' => '',
'groupattr' => 'gid',
'groupoc' => 'groupOfNames',
'groupnameattr' => '',
'groupdescattr' => '',
'groupmemberattr' => '',
'groupmemberisdn' => true,
'usergroupattr' => '',
'groupgroupattr' => '',
'debug' => false
];
protected $logslib = null;
/**
* @var array The user attributes
*/
protected $user_attributes = null;
// Constructor
public function __construct($options)
{
// debug setting
$logslib = TikiLib::lib('logs');
if (isset($options['debug']) && ($options['debug'] === true || $options['debug'] == 'y' ) && ($logslib instanceof LogsLib)) {
$this->options['debug'] = true;
$this->logslib = $logslib;
}
// Configure the connection
// host can be a list of hostnames.
// It is easier to create URIs because if we use ssl, we have to create a URI
if (isset($options['host']) && ! empty($options['host'])) {
$h = $options['host'];
} else { // use default
$h = $this->options['host'];
}
$t = preg_split('#[\s,]#', $h);
if (isset($options['useSsl']) && ($options['useSsl'] == 'y' || $options['useSsl'] === true)) {
$prefix = 'ldaps://';
$port = 636;
} else {
$prefix = 'ldap://';
$port = 389;
}
if (isset($options['port']) && ! empty($options['port'])) {
$port = (int)$options['port'];
}
$this->options['port'] = $port; // its save to set port in URI
$this->options['host'] = [];
foreach ($t as $h) {
if (preg_match('#^ldaps?://#', $h)) { // entry is already URI
$this->options['host'] = $h;
} else {
$this->options['host'] = $h;
}
}
if (isset($options['useStartTls']) && ! empty($options['useStartTls'])) {
$this->options['useStartTls'] = ($options['useStartTls'] === true || $options['useStartTls'] == 'y');
}
if (isset($options['groupmemberisdn']) && ! empty($options['groupmemberisdn'])) {
$this->options['groupmemberisdn'] = ($options['groupmemberisdn'] === true || $options['groupmemberisdn'] == 'y');
}
// only string checking fo these ones
foreach (
['baseDn', 'username', 'password', 'userdn', 'useroc', 'userattr',
'fullnameattr', 'emailattr', 'groupdn', 'groupattr', 'groupoc', 'groupnameattr',
'groupdescattr', 'groupmemberattr', 'usergroupattr', 'groupgroupattr', 'binddn', 'bindpw'] as $n
) {
if (isset($options[$n]) && ! empty($options[$n])) {
$this->options[$n] = $options[$n];
}
}
if (empty($this->options['groupgroupattr'])) {
$this->options['groupgroupattr'] = $this->options['usergroupattr'];
}
if (isset($options['password'])) {
$this->options['bindpw'] = $options['password'];
}
if (isset($options['scope']) && ! empty($options['scope'])) {
switch ($options['scope']) {
case 'sub':
case 'one':
case 'base':
$this->options['scope'] = $options['scope'];
break;
default:
break;
}
}
if (isset($options['bind_type']) && ! empty($options['bind_type'])) {
switch ($options['bind_type']) {
case 'ad':
case 'ol':
case 'full':
case 'plain':
case 'explicit':
$this->options['bind_type'] = $options['bind_type'];
break;
default:
break;
}
}
}
public function __destruct()
{
unset($this->ldaplink);
}
// Do a ldap bind
public function bind($reconnect = false)
{
global $prefs;
// Force the reconnection
if ($this->ldaplink instanceof Ldap && $this->ldaplink->getBoundUser() !== false) {
if ($reconnect === true) {
$this->ldaplink->disconnect();
} else {
return LdapException::LDAP_SUCCESS; // do not try to reconnect since this may lead to huge timeouts
}
}
// Set the bindpw with the options['password']
if ($this->options['bind_type'] != 'explicit') {
$this->options['bindpw'] = $this->options['password'];
}
$user = $this->options['username'];
switch ($this->options['bind_type']) {
case 'ad': // active directory
preg_match_all('/\s*,?dc=\s*([^,]+)/i', $this->options['baseDn'], $t);
$this->options['binddn'] = $user . '@';
if (isset($t[1]) && is_array($t[1])) {
foreach ($t[1] as $domainpart) {
$this->options['binddn'] .= $domainpart . '.';
}
// cut trailing dot
$this->options['binddn'] = substr($this->options['binddn'], 0, -1);
}
// set referrals to 0 to avoid LDAP_OPERATIONS_ERROR
$this->options['options']['LDAP_OPT_REFERRALS'] = 0;
// use user@domain for binding
$this->options['tryUsernameSplit'] = false;
break;
case 'plain': // plain username
$this->options['binddn'] = $user;
break;
case 'full':
$this->options['binddn'] = $this->user_dn($user);
break;
case 'ol': // openldap
$this->options['binddn'] = 'cn=' . $user . ',' . $prefs['auth_ldap_basedn'];
break;
case 'default':
// Anonymous binding
$this->options['binddn'] = '';
$this->options['bindpw'] = '';
break;
case 'explicit':
break;
default:
$this->add_log('ldap', 'Error: Invalid "bind_type" value "' . $this->options['bind_type'] . '".');
die;
}
$this->add_log(
'ldap',
'Connect Host: ' . $this->options['host'] . '. Binddn: ' . $this->options['binddn'] . ' at line ' . __LINE__ . ' in ' . __FILE__
);
$permittedOptions = [
'host',
'port',
'useSsl',
'username',
'password',
'bindRequiresDn',
'baseDn',
'accountCanonicalForm',
'accountDomainName',
'accountDomainNameShort',
'accountFilterFormat',
'allowEmptyPassword',
'useStartTls',
'optReferrals',
'tryUsernameSplit',
'networkTimeout',
];
$options = [];
//create options array to handle it to \Laminas\Ldap\Ldap
foreach ($permittedOptions as $o) {
if (isset($this->options[$o])) {
$options[$o] = $this->options[$o];
}
}
try {
$this->ldaplink = new Ldap($options);
$this->ldaplink->bind($this->options['binddn'], $this->options['bindpw']);
} catch (LdapException $e) {
if ($prefs['auth_ldap_debug'] == 'y') {
$this->add_log('ldap', 'Error: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
}
return $e->getCode();
}
return LdapException::LDAP_SUCCESS;
}
// return information about user attributes
public function get_user_attributes($force_reload = false)
{
if ($force_reload) {
unset($this->user_attributes);
}
if (! empty($this->user_attributes)) {
return $this->user_attributes;
}
$userdn = $this->user_dn();
// ensure we have a connection to the ldap server
if ($this->bind() !== LdapException::LDAP_SUCCESS) {
//@todo fix this error since getMessage no longer works
$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getLastError() . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
// todo: only fetch needed attributes
//A non-existing user may not return ldaplink->getEntry (found bug on windows server), if not found, user input incorrect username/password
try {
$searchresult = $this->ldaplink->search("(objectClass=*)", $userdn, Ldap::SEARCH_SCOPE_BASE, [], null);
$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
$entry = $searchresult->getFirst();
} catch (LdapException $e) {
$entry = null;
}
if ($force_reload || is_null($entry)) { // wrong userdn. So we have to search
// prepare Search Filter
$filter = Filter::equals($this->options['userattr'], $this->options['username']);
$this->add_log('ldap', 'Searching for user information with filter: ' . $filter->toString() . ' at line ' . __LINE__ . ' in ' . __FILE__);
try {
$searchresult = $this->ldaplink->search($filter, $this->userbase_dn(), $this->options['scope']);
$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
} catch (LdapException $e) {
$this->add_log('ldap', 'Search failed: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
if ($searchresult->count() != 1) {
$this->add_log('ldap', 'Error: Search returned ' . $searchresult->count() . ' entries' . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
// get first entry
$entry = $searchresult->getFirst();
}
$this->user_attributes = $this->parseLdapAttributes($entry);
if (empty($this->user_attributes)) {
$this->add_log('ldap', 'Error fetching user attributes at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
return $this->user_attributes;
}
// Request all users attributes
public function get_all_users_attributes()
{
// ensure we have a connection to the ldap server
if ($this->bind() !== LdapException::LDAP_SUCCESS) {
$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getLastError() . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
// Prepare Search Filter
$filter = Filter::equals('objectclass', $this->options['useroc']);
$this->add_log('ldap', 'Searching for user information with filter: ' . $filter->toString() . ' at line ' . __LINE__ . ' in ' . __FILE__);
try {
$searchresult = $this->ldaplink->search($filter, $this->userbase_dn(), $this->options['scope']);
$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
} catch (LdapException $e) {
$this->add_log('ldap', 'Search failed: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
if ($searchresult->count() < 1) {
$this->add_log('ldap', 'Error: Search returned ' . $searchresult->count() . ' entries' . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
$entries = $searchresult->toArray();
$users_attributes = [];
foreach ($entries as $entry) {
$users_attributes[] = $this->parseLdapAttributes($entry);
}
return ($users_attributes);
}
// return dn of all groups a user belongs to
public function get_groups($force_reload = false)
{
$this->get_user_attributes($force_reload);
// ensure we have a connection to the ldap server
if ($this->bind() !== LdapException::LDAP_SUCCESS) {
$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getLastError() . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
$filter1 = Filter::equals('objectClass', $this->options['groupoc']);
if (! empty($this->options['groupmemberattr'])) {
// get membership from group information
if ($this->options['groupmemberisdn']) {
if ($this->user_attributes['dn'] == null) {
return false;
}
$filter2 = Filter::equals($this->options['groupmemberattr'], $this->user_dn()) ;
} else {
$filter2 = Filter::equals($this->options['groupmemberattr'], $this->options['username']);
}
$filter = Filter::andFilter($filter1, $filter2);
} elseif (! empty($this->options['usergroupattr'])) {
// get membership from user information
if ($this->options['usergroupattr'] === 'distinguishedName') {
// get membership from user DN
// split DN into RDN strings
$dn_string = $this->user_attributes[$this->options['usergroupattr']];
$rdn_strings = explode(',', $dn_string);
// add value of RDNs with OU type
$ugi = [];
foreach ($rdn_strings as $rdn_string) {
// split RDN string in type and value
$rdn_parts = explode('=', $rdn_string, 2);
$rdn_type = $rdn_parts[0];
$rdn_value = $rdn_parts[1];
// add RDN value if type is OU
if (strtoupper($rdn_type) === 'OU') {
$ugi[] = $rdn_value;
}
}
} else {
$ugi = &$this->user_attributes[$this->options['usergroupattr']];
}
if (! empty($ugi)) {
if (! is_array($ugi)) {
$ugi = [$ugi];
}
if (count($ugi) == 1) { // one gid
$filter3 = Filter::equals($this->options['groupgroupattr'], $ugi[0]);
} else { // mor gids
$filtertmp = [];
foreach ($ugi as $g) {
$filtertmp[] = Filter::equals($this->options['groupgroupattr'], $g);
}
$filter3 = new Filter\OrFilter($filtertmp);
}
$filter = Filter::andFilter($filter1, $filter3);
} else { // User has no group
return [];
}
} else {
// not possible to get groups - return empty array
return [];
}
$this->add_log(
'ldap',
'Searching for group entries with filter: ' . $filter->toString() . ' base ' .
$this->groupbase_dn() . ' at line ' . __LINE__ . ' in ' . __FILE__
);
try {
$searchresult = $this->ldaplink->search($filter, $this->groupbase_dn(), $this->options['scope']);
$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
} catch (LdapException $e) {
$this->add_log('ldap', 'Search failed: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
return false;
}
$this->add_log('ldap', 'Found ' . $searchresult->count() . ' entries. Extracting entries now.');
$groupEntries = $searchresult->toArray();
$this->groups = [];
foreach ($groupEntries as $entry) {
if (empty($entry)) {
continue;
}
$group = $this->parseLdapAttributes($entry);
$this->groups[$group['dn']] = $group; // no error checking necessary here
}
$this->add_log('ldap', count($this->groups) . ' groups found at line ' . __LINE__ . ' in ' . __FILE__);
return($this->groups);
}
// helper functions
private function userbase_dn()
{
if (empty($this->options['userdn'])) {
return($this->options['baseDn']);
}
return($this->options['userdn'] . ',' . $this->options['baseDn']);
}
private function user_dn()
{
if (isset($this->user_attributes['dn'])) {
// we did already fetch user attributes and have the real dn now
return($this->user_attributes['dn']);
}
if (empty($this->options['userattr'])) {
$ua = 'cn=';
} else {
$ua = $this->options['userattr'] . '=';
}
return($ua . $this->options['username'] . ',' . $this->userbase_dn());
}
private function groupbase_dn()
{
if (empty($this->options['groupdn'])) {
return($this->options['baseDn']);
}
return($this->options['groupdn'] . ',' . $this->options['baseDn']);
}
private function add_log($facility, $message)
{
if ($this->options['debug']) {
$this->logslib->add_log($facility, $message);
}
}
/**
* Setter to set an option value
* @param string $name The name of the option
* @param mixed $value The value
* @return void
* @throw Exception
*/
public function setOption($name, $value = null)
{
if (isset($this->options[$name])) {
$this->options[$name] = $value;
} else {
throw new Exception(sprintf("Undefined option: %s \n", $name), E_USER_WARNING);
}
}
/**
* Return the value of the attribute past in param
* @param string $name The name of the attribute
* @return mixed
* @throw Exception
*/
public function getUserAttribute($name)
{
$value = '';
try {
$values = self::get_user_attributes();
if (isset($values[$name])) {
$value = $values[$name];
} else {
throw new Exception(sprintf("Undefined attribute %s \n", $name), E_USER_WARNING);
}
} catch (Exception $e) {
}
return $value;
}
/**
* Parse the ldap retrieved attributes for a given entry
*
* @param $entry
* @return array
*/
private function parseLdapAttributes($entry)
{
$attributes = [];
foreach ($entry as $key => $value) {
if (is_array($value)) {
$attributes[$key] = count($value) == 1 ? array_shift($value) : $value;
} else {
$attributes[$key] = $value;
}
}
return $attributes;
}
}