code/services/LDAPService.php
public function updateMemberFromLDAP(Member $member, $data = null)
    {
        if (!$this->enabled()) {
            return false;
        }

        if (!$member->GUID) {
            SS_Log::log(sprintf('Cannot update Member ID %s, GUID not set', $member->ID), SS_Log::WARN);
            return false;
        }

        if (!$data) {
            $data = $this->getUserByGUID($member->GUID);
            if (!$data) {
                SS_Log::log(sprintf('Could not retrieve data for user. GUID: %s', $member->GUID), SS_Log::WARN);
                return false;
            }
        }

        $member->IsExpired = ($data['useraccountcontrol'] & 2) == 2;
        $member->LastSynced = (string)SS_Datetime::now();

        foreach ($member->config()->ldap_field_mappings as $attribute => $field) {
            if (!isset($data[$attribute])) {
                SS_Log::log(sprintf(
                    'Attribute %s configured in Member.ldap_field_mappings, but no available attribute in AD data (GUID: %s, Member ID: %s)',
                    $attribute,
                    $data['objectguid'],
                    $member->ID
                ), SS_Log::NOTICE);

                continue;
            }

            if ($attribute == 'thumbnailphoto') {
                $imageClass = $member->getRelationClass($field);
                if ($imageClass !== 'Image' && !is_subclass_of($imageClass, 'Image')) {
                    SS_Log::log(sprintf(
                        'Member field %s configured for thumbnailphoto AD attribute, but it isn\'t a valid relation to an Image class',
                        $field
                    ), SS_Log::WARN);

                    continue;
                }

                $filename = sprintf('thumbnailphoto-%s.jpg', $data['samaccountname']);
                $path = ASSETS_DIR . '/' . $member->config()->ldap_thumbnail_path;
                $absPath = BASE_PATH . '/' . $path;
                if (!file_exists($absPath)) {
                    Filesystem::makeFolder($absPath);
                }

                // remove existing record if it exists
                $existingObj = $member->getComponent($field);
                if ($existingObj && $existingObj->exists()) {
                    $existingObj->delete();
                }

                // The image data is provided in raw binary.
                file_put_contents($absPath . '/' . $filename, $data[$attribute]);
                $record = new $imageClass();
                $record->Name = $filename;
                $record->Filename = $path . '/' . $filename;
                $record->write();

                $relationField = $field . 'ID';
                $member->{$relationField} = $record->ID;
            } else {
                $member->$field = $data[$attribute];
            }
        }

        // if a default group was configured, ensure the user is in that group
        if ($this->config()->default_group) {
            $group = Group::get()->filter('Code', $this->config()->default_group)->limit(1)->first();
            if (!($group && $group->exists())) {
                SS_Log::log(
                    sprintf(
                        'LDAPService.default_group misconfiguration! There is no such group with Code = \'%s\'',
                        $this->config()->default_group
                    ),
                    SS_Log::WARN
                );
            } else {
                $group->Members()->add($member, [
                    'IsImportedFromLDAP' => '1'
                ]);
            }
        }

        // this is to keep track of which groups the user gets mapped to
        // and we'll use that later to remove them from any groups that they're no longer mapped to
        $mappedGroupIDs = [];

        // Member must have an ID before manipulating Groups, otherwise they will not be added correctly.
        // However we cannot do a full ->write before the groups are associated, because this will upsync
        // the Member, in effect deleting all their LDAP group associations!
        $member->writeWithoutSync();

        // ensure the user is in any mapped groups
        if (isset($data['memberof'])) {
            $ldapGroups = is_array($data['memberof']) ? $data['memberof'] : [$data['memberof']];
            foreach ($ldapGroups as $groupDN) {
                foreach (LDAPGroupMapping::get() as $mapping) {
                    if (!$mapping->DN) {
                        SS_Log::log(
                            sprintf(
                                'LDAPGroupMapping ID %s is missing DN field. Skipping',
                                $mapping->ID
                            ),
                            SS_Log::WARN
                        );
                        continue;
                    }

                    // the user is a direct member of group with a mapping, add them to the SS group.
                    if ($mapping->DN == $groupDN) {
                        $group = $mapping->Group();
                        if ($group && $group->exists()) {
                            $group->Members()->add($member, [
                                'IsImportedFromLDAP' => '1'
                            ]);
                            $mappedGroupIDs[] = $mapping->GroupID;
                        }
                    }

                    // the user *might* be a member of a nested group provided the scope of the mapping
                    // is to include the entire subtree. Check all those mappings and find the LDAP child groups
                    // to see if they are a member of one of those. If they are, add them to the SS group
                    if ($mapping->Scope == 'Subtree') {
                        $childGroups = $this->getNestedGroups($mapping->DN, ['dn']);
                        if (!$childGroups) {
                            continue;
                        }

                        foreach ($childGroups as $childGroupDN => $childGroupRecord) {
                            if ($childGroupDN == $groupDN) {
                                $group = $mapping->Group();
                                if ($group && $group->exists()) {
                                    $group->Members()->add($member, [
                                        'IsImportedFromLDAP' => '1'
                                    ]);
                                    $mappedGroupIDs[] = $mapping->GroupID;
                                }
                            }
                        }
                    }
                }
            }
        }

        // remove the user from any previously mapped groups, where the mapping has since been removed
        $groupRecords = DB::query(sprintf('SELECT "GroupID" FROM "Group_Members" WHERE "IsImportedFromLDAP" = 1 AND "MemberID" = %s', $member->ID));
        foreach ($groupRecords as $groupRecord) {
            if (!in_array($groupRecord['GroupID'], $mappedGroupIDs)) {
                $group = Group::get()->byId($groupRecord['GroupID']);
                // Some groups may no longer exist. SilverStripe does not clean up join tables.
                if ($group) {
                    $group->Members()->remove($member);
                }
            }
        }
        // This will throw an exception if there are two distinct GUIDs with the same email address.
        // We are happy with a raw 500 here at this stage.
        $member->write();
    }
public function updateMemberFromLDAP(Member $member, $data = null)
    {
        if (!$this->enabled()) {
            return false;
        }

        if (!$member->GUID) {
            SS_Log::log(sprintf('Cannot update Member ID %s, GUID not set', $member->ID), SS_Log::WARN);
            return false;
        }

        if (!$data) {
            $data = $this->getUserByGUID($member->GUID);
            if (!$data) {
                SS_Log::log(sprintf('Could not retrieve data for user. GUID: %s', $member->GUID), SS_Log::WARN);
                return false;
            }
        }

        $member->IsExpired = ($data['useraccountcontrol'] & 2) == 2;
        $member->LastSynced = (string)SS_Datetime::now();

        foreach ($member->config()->ldap_field_mappings as $attribute => $field) {
            if (!isset($data[$attribute])) {
                SS_Log::log(sprintf(
                    'Attribute %s configured in Member.ldap_field_mappings, but no available attribute in AD data (GUID: %s, Member ID: %s)',
                    $attribute,
                    $data['objectguid'],
                    $member->ID
                ), SS_Log::NOTICE);

                continue;
            }

            if ($attribute == 'thumbnailphoto') {
                $imageClass = $member->getRelationClass($field);
                if ($imageClass !== 'Image' && !is_subclass_of($imageClass, 'Image')) {
                    SS_Log::log(sprintf(
                        'Member field %s configured for thumbnailphoto AD attribute, but it isn\'t a valid relation to an Image class',
                        $field
                    ), SS_Log::WARN);

                    continue;
                }

                $filename = sprintf('thumbnailphoto-%s.jpg', $data['samaccountname']);
                $path = ASSETS_DIR . '/' . $member->config()->ldap_thumbnail_path;
                $absPath = BASE_PATH . '/' . $path;
                if (!file_exists($absPath)) {
                    Filesystem::makeFolder($absPath);
                }

                // remove existing record if it exists
                $existingObj = $member->getComponent($field);
                if ($existingObj && $existingObj->exists()) {
                    $existingObj->delete();
                }

                // The image data is provided in raw binary.
                file_put_contents($absPath . '/' . $filename, $data[$attribute]);
                $record = new $imageClass();
                $record->Name = $filename;
                $record->Filename = $path . '/' . $filename;
                $record->write();

                $relationField = $field . 'ID';
                $member->{$relationField} = $record->ID;
            } else {
                $member->$field = $data[$attribute];
            }
        }

        // if a default group was configured, ensure the user is in that group
        if ($this->config()->default_group) {
            $group = Group::get()->filter('Code', $this->config()->default_group)->limit(1)->first();
            if (!($group && $group->exists())) {
                SS_Log::log(
                    sprintf(
                        'LDAPService.default_group misconfiguration! There is no such group with Code = \'%s\'',
                        $this->config()->default_group
                    ),
                    SS_Log::WARN
                );
            } else {
                $group->Members()->add($member, [
                    'IsImportedFromLDAP' => '1'
                ]);
            }
        }

        // this is to keep track of which groups the user gets mapped to
        // and we'll use that later to remove them from any groups that they're no longer mapped to
        $mappedGroupIDs = [];

        // Member must have an ID before manipulating Groups, otherwise they will not be added correctly.
        // However we cannot do a full ->write before the groups are associated, because this will upsync
        // the Member, in effect deleting all their LDAP group associations!
        $member->writeWithoutSync();

        // ensure the user is in any mapped groups
        if (isset($data['memberof'])) {
            $ldapGroups = is_array($data['memberof']) ? $data['memberof'] : [$data['memberof']];
            foreach ($ldapGroups as $groupDN) {
                foreach (LDAPGroupMapping::get() as $mapping) {
                    if (!$mapping->DN) {
                        SS_Log::log(
                            sprintf(
                                'LDAPGroupMapping ID %s is missing DN field. Skipping',
                                $mapping->ID
                            ),
                            SS_Log::WARN
                        );
                        continue;
                    }

                    // the user is a direct member of group with a mapping, add them to the SS group.
                    if ($mapping->DN == $groupDN) {
                        $group = $mapping->Group();
                        if ($group && $group->exists()) {
                            $group->Members()->add($member, [
                                'IsImportedFromLDAP' => '1'
                            ]);
                            $mappedGroupIDs[] = $mapping->GroupID;
                        }
                    }

                    // the user *might* be a member of a nested group provided the scope of the mapping
                    // is to include the entire subtree. Check all those mappings and find the LDAP child groups
                    // to see if they are a member of one of those. If they are, add them to the SS group
                    if ($mapping->Scope == 'Subtree') {
                        $childGroups = $this->getNestedGroups($mapping->DN, ['dn']);
                        if (!$childGroups) {
                            continue;
                        }

                        foreach ($childGroups as $childGroupDN => $childGroupRecord) {
                            if ($childGroupDN == $groupDN) {
                                $group = $mapping->Group();
                                if ($group && $group->exists()) {
                                    $group->Members()->add($member, [
                                        'IsImportedFromLDAP' => '1'
                                    ]);
                                    $mappedGroupIDs[] = $mapping->GroupID;
                                }
                            }
                        }
                    }
                }
            }
        }

        // remove the user from any previously mapped groups, where the mapping has since been removed
        $groupRecords = DB::query(sprintf('SELECT "GroupID" FROM "Group_Members" WHERE "IsImportedFromLDAP" = 1 AND "MemberID" = %s', $member->ID));
        foreach ($groupRecords as $groupRecord) {
            if (!in_array($groupRecord['GroupID'], $mappedGroupIDs)) {
                $group = Group::get()->byId($groupRecord['GroupID']);
                // Some groups may no longer exist. SilverStripe does not clean up join tables.
                if ($group) {
                    $group->Members()->remove($member);
                }
            }
        }
        // This will throw an exception if there are two distinct GUIDs with the same email address.
        // We are happy with a raw 500 here at this stage.
        $member->write();
    }
Update the current Member record with data from LDAP.

It's allowed to pass an unwritten Member record here, because it's not always possible to satisfy
field constraints without importing data from LDAP (for example if the application requires Username
through a Validator). Even though unwritten, it still must have the GUID set.

Constraints:
- GUID of the member must have already been set, for integrity reasons we don't allow it to change here.

@param Member
@param array|null $data If passed, this is pre-existing AD attribute data to update the Member with.
If not given, the data will be looked up by the user's GUID.
@return bool