public function run($request) { // get all groups from LDAP, but only get the attributes we need. // this is useful to avoid holding onto too much data in memory // especially in the case where getGroups() would return a lot of groups $ldapGroups = $this->ldapService->getGroups(false, array('objectguid', 'samaccountname', 'dn', 'name', 'description'), 'objectguid'); $start = time(); $count = 0; foreach ($ldapGroups as $data) { $group = Group::get()->filter('GUID', $data['objectguid'])->limit(1)->first(); if (!($group && $group->exists())) { // create the initial Group with some internal fields $group = new Group(); $group->GUID = $data['objectguid']; $this->log(sprintf('Creating new Group (ID: %s, GUID: %s, sAMAccountName: %s)', $group->ID, $data['objectguid'], $data['samaccountname'])); } else { $this->log(sprintf('Updating existing Group "%s" (ID: %s, GUID: %s, sAMAccountName: %s)', $group->getTitle(), $group->ID, $data['objectguid'], $data['samaccountname'])); } // Synchronise specific guaranteed fields. $group->Code = $data['samaccountname']; if (!empty($data['name'])) { $group->Title = $data['name']; } else { $group->Title = $data['samaccountname']; } if (!empty($data['description'])) { $group->Description = $data['description']; } $group->DN = $data['dn']; $group->LastSynced = (string) SS_Datetime::now(); $group->IsImportedFromLDAP = true; $group->write(); // Mappings on this group are automatically maintained to contain just the group's DN. // First, scan through existing mappings and remove ones that are not matching (in case the group moved). $hasCorrectMapping = false; foreach ($group->LDAPGroupMappings() as $mapping) { if ($mapping->DN === $data['dn']) { // This is the correct mapping we want to retain. $hasCorrectMapping = true; } else { $this->log(sprintf('Deleting invalid mapping %s on %s.', $mapping->DN, $group->getTitle())); $mapping->delete(); } } // Second, if the main mapping was not found, add it in. if (!$hasCorrectMapping) { $this->log(sprintf('Setting up missing group mapping from %s to %s', $group->getTitle(), $data['dn'])); $mapping = new LDAPGroupMapping(); $mapping->DN = $data['dn']; $mapping->write(); $group->LDAPGroupMappings()->add($mapping); } // cleanup object from memory $group->destroy(); $count++; } // remove Group records that were previously imported, but no longer exist in the directory // NOTE: DB::query() here is used for performance and so we don't run out of memory if ($this->config()->destructive) { foreach (DB::query('SELECT "ID", "GUID" FROM "Group" WHERE "IsImportedFromLDAP" = 1') as $record) { if (!isset($ldapGroups[$record['GUID']])) { $group = Group::get()->byId($record['ID']); // Cascade into mappings, just to clean up behind ourselves. foreach ($group->LDAPGroupMappings() as $mapping) { $mapping->delete(); } $group->delete(); $this->log(sprintf('Removing Group "%s" (GUID: %s) that no longer exists in LDAP.', $group->Title, $group->GUID)); // cleanup object from memory $group->destroy(); } } } $end = time() - $start; $this->log(sprintf('Done. Processed %s records. Duration: %s seconds', $count, round($end, 0))); }
/** * Update the current Member record with data from LDAP. * * Constraints: * - Member *must* be in the database before calling this as it will need the ID to be mapped to a {@link Group}. * - 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 */ public function updateMemberFromLDAP($member, $data = null) { // don't attempt to do this if there's no LDAP configured if (!Config::inst()->get('LDAPGateway', 'options')) { 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(); $member->IsImportedFromLDAP = true; 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::WARN); 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, array('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 = array(); // ensure the user is in any mapped groups if (isset($data['memberof'])) { $ldapGroups = is_array($data['memberof']) ? $data['memberof'] : array($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) { $mapping->Group()->Members()->add($member, array('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, array('dn')); if (!$childGroups) { continue; } foreach ($childGroups as $childGroupDN => $childGroupRecord) { if ($childGroupDN == $groupDN) { $mapping->Group()->Members()->add($member, array('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(); }
/** * Sync a specific Group by updating it with LDAP data. * * @param Group $group An existing Group or a new Group object * @param array $data LDAP group object data * * @return bool */ public function updateGroupFromLDAP(Group $group, $data) { if (!$this->enabled()) { return false; } // Synchronise specific guaranteed fields. $group->Code = $data['samaccountname']; if (!empty($data['name'])) { $group->Title = $data['name']; } else { $group->Title = $data['samaccountname']; } if (!empty($data['description'])) { $group->Description = $data['description']; } $group->DN = $data['dn']; $group->LastSynced = (string) SS_Datetime::now(); $group->IsImportedFromLDAP = true; $group->write(); // Mappings on this group are automatically maintained to contain just the group's DN. // First, scan through existing mappings and remove ones that are not matching (in case the group moved). $hasCorrectMapping = false; foreach ($group->LDAPGroupMappings() as $mapping) { if ($mapping->DN === $data['dn']) { // This is the correct mapping we want to retain. $hasCorrectMapping = true; } else { $mapping->delete(); } } // Second, if the main mapping was not found, add it in. if (!$hasCorrectMapping) { $mapping = new LDAPGroupMapping(); $mapping->DN = $data['dn']; $mapping->write(); $group->LDAPGroupMappings()->add($mapping); } }
public function MappedGroups() { return LDAPGroupMapping::get(); }