Skip to content

Commit 02dce45

Browse files
author
Robin Kluth
committed
feat(searchUser): New Param baseDN which overrides the default baseDN
fix(search): Suppress `ldap_search`'s errors in favor of Yii's `::error` logging feat(searchGroup): Add new function `searchGroup` which helps finding groups and (optionally) its members feat(attributes): Check which attributes are single-valued to always return an array (even with single value) for non-single-valued attributes. This avoids returning attributes as array(non-array per entry. fix(results): Return multi-value attribute values as array sintead of conact them. fix(sid): Fix generating small SIDs
1 parent 5ffbca1 commit 02dce45

File tree

1 file changed

+111
-18
lines changed

1 file changed

+111
-18
lines changed

src/LdapAuth.php

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class LdapAuth extends BaseObject
3737

3838
/**
3939
* If false (default) any user search would return the whole result.
40-
* If true, the script checks every users sidHistory and only return results which are newer (migrated).
40+
* If true, the script checks every user sidHistory and only return results which are newer (migrated).
4141
* A use case for `true`: You have two domains and user "Foo" was copied from Domain 1 to Domain 2 without deleting it from Domain 1 - now you have 2 results for a search "Foo", but the entry in Domain 2 has a set "sidHistory" with its sid from Domain 1.
4242
* Setting this tp true will filter out the "Foo" from Domain 1, since its sid is listed in the Domain 2 entry of it.
4343
*
@@ -69,6 +69,9 @@ class LdapAuth extends BaseObject
6969
private $_l;
7070
private $_username;
7171
private $_curDn;
72+
private $_curDomainHostname;
73+
74+
private $_singleValuedAttrs;
7275

7376
public function init()
7477
{
@@ -308,6 +311,7 @@ public function login($username, $password, $domainKey = false, $fetchUserDN = f
308311
$this->_l = $l;
309312
$this->_ldapBaseDn = $domainData['baseDn'];
310313
$this->_username = $username;
314+
$this->_curDomainHostname = $domainData['hostname'];
311315

312316
return true;
313317
}
@@ -355,7 +359,7 @@ public function fetchUserData($attributes = "")
355359
}
356360
$sid = self::SIDtoString($entries[0]['objectsid'])[0];
357361
$sidHistory = isset($entries[0]['sidhistory']) ? self::SIDtoString($entries[0]['sidhistory']) : null;
358-
return array_merge(['sid' => $sid, 'sidhistory' => $sidHistory], self::handleEntry($entries[0]));
362+
return array_merge(['sid' => $sid, 'sidhistory' => $sidHistory], $this->handleEntry($entries[0], $dom));
359363
} else {
360364
Yii::error('[FetchUserData]: Search failed: ' . ldap_error($this->_l), __METHOD__);
361365
return false;
@@ -366,13 +370,14 @@ public function fetchUserData($attributes = "")
366370
* @param string|null $searchFor Search-Term
367371
* @param array|null $attributes Attributes to get back
368372
* @param string|null $searchFilter Filter string. Set %searchFor% als placeholder to search for $searchFor
369-
* @param integer $domainKey You can provide integer domainkey, this is then used as target domain! Otherwise it searches in all domains
373+
* @param int|null $domainKey You can provide integer domainkey, this is then used as target domain! Otherwise it searches in all domains
370374
* @param bool $onlyActiveAccounts SHould the search result only contain active accounts? => https://www.der-windows-papst.de/2016/12/18/active-directory-useraccountcontrol-values/
371375
* @param bool $allDomainsHaveToBeReachable True: All configured domains need to be reachable in order to get a result. If one is not reachable, false will be returned
376+
* @param string|null $baseDN Use given BaseDN instead of configured one. This normally requires the exact domainKey being set as well.
372377
* @return array|false An Array with the results, indexed by their SID - false if an ERROR occured!
373-
* @throws \InvalidArgumentException
378+
* @throws ErrorException
374379
*/
375-
public function searchUser(?string $searchFor, ?array $attributes = [], ?string $searchFilter = "", bool $domainKey = false, bool $onlyActiveAccounts = false, bool $allDomainsHaveToBeReachable = false)
380+
public function searchUser(?string $searchFor, ?array $attributes = [], ?string $searchFilter = "", ?int $domainKey = null, bool $onlyActiveAccounts = false, bool $allDomainsHaveToBeReachable = false, ?string $baseDN = null)
376381
{
377382

378383
if (empty($attributes)) {
@@ -452,6 +457,7 @@ public function searchUser(?string $searchFor, ?array $attributes = [], ?string
452457
}
453458

454459
$searchFilter = str_replace(["%searchFor%", "%onlyActive%"], [addslashes($searchFor), $onlyActive], $searchFilter);
460+
$baseDN = $baseDN ?: $this->_ldapBaseDn;
455461

456462
Yii::debug('Search-Filter: ' . $searchFilter, __METHOD__);
457463

@@ -460,6 +466,43 @@ public function searchUser(?string $searchFor, ?array $attributes = [], ?string
460466
Yii::debug("Supported Controls here:", __METHOD__);
461467
Yii::debug($supControls, __METHOD__);
462468

469+
if (empty($this->_singleValuedAttrs) || !isset($this->_singleValuedAttrs[$domain['hostname']])) {
470+
$this->_singleValuedAttrs[$domain['hostname']] = [];
471+
Yii::info("Getting attribute type definitions for this domain!", __METHOD__);
472+
473+
$result = @ldap_read($this->_l, '', "(objectClass=*)", ["subschemaSubentry"]);
474+
if ($result) {
475+
$subSchema = ldap_get_entries($this->_l, $result);
476+
Yii::debug("Subschema entry:", __METHOD__);
477+
Yii::debug($subSchema, __METHOD__);
478+
479+
if (isset($subSchema[0]['subschemasubentry'][0])) {
480+
481+
$result = @ldap_read($this->_l, $subSchema[0]['subschemasubentry'][0], "(objectClass=*)", ["attributeTypes"]);
482+
483+
if ($result) {
484+
$entries = ldap_get_entries($this->_l, $result);
485+
foreach ($entries[0]['attributetypes'] as $key => $definition) {
486+
if (stripos($definition, 'SINGLE-VALUE') !== false) {
487+
$match = preg_match("/NAME ['\"](.*?)['\"]/", $definition, $matches);
488+
if ($match && isset($matches[1])) {
489+
$this->_singleValuedAttrs[$domain['hostname']][] = $matches[1];
490+
}
491+
}
492+
}
493+
} else {
494+
Yii::warning("Could not read attribute Types" . ldap_error($this->_l), __METHOD__);
495+
}
496+
} else {
497+
Yii::warning("No subschema entry found!", __METHOD__);
498+
}
499+
} else {
500+
Yii::warning("Could not read subschema entry: " . ldap_error($this->_l), __METHOD__);
501+
}
502+
}
503+
504+
505+
463506

464507
$cookie = '';
465508
$requestControls = [];
@@ -473,14 +516,15 @@ public function searchUser(?string $searchFor, ?array $attributes = [], ?string
473516
}
474517

475518
do {
476-
$result = ldap_search($this->_l, $this->_ldapBaseDn, $searchFilter, $attributes, 0, -1, -1, LDAP_DEREF_NEVER, $requestControls);
519+
$result = @ldap_search($this->_l, $baseDN, $searchFilter, $attributes, 0, -1, -1, LDAP_DEREF_NEVER, $requestControls);
477520
if (!$result) {
478521
// Something is wrong with the search query
479522
if (is_null($this->_l)) {
480-
Yii::warning('ldap_search_error: null', __FUNCTION__);
523+
Yii::error('ldap_search_error: null', __METHOD__);
481524
} else {
482-
Yii::warning('ldap_search_error: ' . ldap_error($this->_l), __FUNCTION__);
525+
Yii::error('ldap_search_error: ' . ldap_error($this->_l), __METHOD__);
483526
}
527+
Yii::error("Search query: " . $searchFilter, __METHOD__);
484528
break;
485529
}
486530
ldap_parse_result($this->_l, $result, $errcode, $matcheddn, $errmsg, $referrals, $controls);
@@ -526,7 +570,7 @@ public function searchUser(?string $searchFor, ?array $attributes = [], ?string
526570
// Enable domainName output if more than one domains configured
527571
$additionalData['domainName'] = $this->domains[$i]['name'];
528572
}
529-
$return[$sid] = array_merge($additionalData, self::handleEntry($entry));
573+
$return[$sid] = array_merge($additionalData, $this->handleEntry($entry));
530574
}
531575
}
532576

@@ -577,6 +621,45 @@ public function searchUser(?string $searchFor, ?array $attributes = [], ?string
577621

578622
}
579623

624+
/**
625+
* Searches directly for groups and optionally return its members
626+
* @param string|null $searchFor The raw (!) LDAP-Filter. Like (&(objectCategory=group) (|(objectSid=%searchFor%)(cn=*%searchFor%*)))
627+
* @param array|null $attributes
628+
* @param bool $returnMembers Should the function fetch the group members?
629+
* @param int|null $domainKey
630+
* @param bool $onlyActiveAccounts
631+
* @param bool $allDomainsHaveToBeReachable
632+
* @return array|false
633+
* @throws ErrorException
634+
*/
635+
public function searchGroup(?string $searchFor, ?array $attributes = ['dn', 'member'], bool $returnMembers = false, ?int $domainKey = null, bool $onlyActiveAccounts = false, bool $allDomainsHaveToBeReachable = false)
636+
{
637+
$groups = $this->searchUser(null, $attributes, $searchFor, $domainKey, $onlyActiveAccounts, $allDomainsHaveToBeReachable);
638+
639+
if (!$returnMembers) {
640+
return $groups;
641+
}
642+
643+
foreach ($groups as $gkey => $group) {
644+
if (!isset($group['member'])) {
645+
continue;
646+
}
647+
if (is_string($group['member'])) {
648+
$group['member'] = [$group['member']];
649+
}
650+
$groups[$gkey]['users'] = [];
651+
foreach ($group['member'] as $key => $member) {
652+
if ($key == 'count') {
653+
continue;
654+
}
655+
$groups[$gkey]['users'] = array_merge($groups[$gkey]['users'], $this->searchUser(null, ['dn'], '(&(objectCategory=person))', $group['domainKey'], false, false, $member));
656+
}
657+
658+
}
659+
660+
return $groups;
661+
}
662+
580663
/**
581664
* Performs attribute updates (with special handling of a few attributes, like unicodepwd). A previous ->login is required!
582665
* @param array $attributes The attribute (array keys are the attribute names, the array values are the attribute values)
@@ -597,7 +680,7 @@ public function updateAttributes($attributes, $dn = null)
597680
}
598681

599682
foreach ($attributes as $attribute => $value) {
600-
Yii::info('Processing attribute ' . $attribute, __FUNCTION__);
683+
Yii::debug('Processing attribute ' . $attribute, __FUNCTION__);
601684

602685
switch ($attribute) {
603686
case 'unicodepwd':
@@ -641,29 +724,39 @@ public static function SIDtoString($ADsid)
641724
$subauths = hexdec($sidinhex[7]);
642725
//Loop through Sub Authorities
643726
for ($i = 0; $i < $subauths; $i++) {
644-
$start = 8 + (4 * $i);
645-
// X amount of 32Bit (4 Byte) Sub Authorities
646-
$sid = $sid . "-" . hexdec($sidinhex[$start + 3] . $sidinhex[$start + 2] . $sidinhex[$start + 1] . $sidinhex[$start]);
727+
try {
728+
$start = 8 + (4 * $i);
729+
// X amount of 32Bit (4 Byte) Sub Authorities
730+
$sid = $sid . "-" . hexdec($sidinhex[$start + 3] . $sidinhex[$start + 2] . $sidinhex[$start + 1] . $sidinhex[$start]);
731+
} catch (\Exception $ex) {
732+
continue;
733+
}
647734
}
648735
Yii::debug('Converted SID to: ' . $sid, __METHOD__);
649736
array_push($results, $sid);
650737
}
651738
return $results;
652739
}
653740

654-
public static function handleEntry($entry)
741+
private function handleEntry($entry)
655742
{
656743
$newEntry = [];
657744
foreach ($entry as $attr => $value) {
745+
Yii::debug('Processing attribute ' . $attr, __FUNCTION__);
746+
658747
if (is_int($attr) || $attr == 'objectsid' || $attr == 'sidhistory' || !isset($value['count'])) {
748+
Yii::debug('Skipping...', __FUNCTION__);
659749
continue;
660750
}
661751
$count = $value['count'];
662-
$newVal = "";
663-
for ($i = 0; $i < $count; $i++) {
664-
$newVal .= $value[$i]; // Concat? Wouldnt it be better to return an array with all values??
752+
Yii::debug('Count: ' . $count, __FUNCTION__);
753+
754+
if ($count > 1 || !in_array($attr, $this->_singleValuedAttrs[$this->_curDomainHostname] ?? [])) {
755+
unset($value['count']);
756+
$newEntry[$attr] = $value; // Return value as is, because it contains multiple entries
757+
} else {
758+
$newEntry[$attr] = $value[0]; // extract first result, because it's the only
665759
}
666-
$newEntry[$attr] = $newVal;
667760
}
668761
return $newEntry;
669762
}

0 commit comments

Comments
 (0)