diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php old mode 100755 new mode 100644 index a7941050f8..006d407e1d --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -1,14 +1,24 @@ + * + * @since 5.0.0 + */ class LdapSync extends Command { /** @@ -16,23 +26,79 @@ class LdapSync extends Command * * @var string */ - protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=} {--base_dn=} {--summary} {--json_summary}'; + protected $signature = 'snipeit:ldap-sync + {--location= : A location name } + {--location_id= : A location id} + {--base_dn= : A diffrent base DN to use } + {--summary : Print summary } + {--json_summary : Print summary in json format } + {--dryrun : Run the sync process but don\'t update the database}'; /** * The console command description. * * @var string */ - protected $description = 'Command line LDAP sync'; + protected $description = 'Command line LDAP/AD sync'; + + /** + * An LdapAd instance. + * + * @var \App\Models\LdapAd + */ + private $ldap; + + /** + * LDAP settings collection. + * + * @var \Illuminate\Support\Collection + */ + private $settings = null; + + /** + * A default location collection. + * + * @var \Illuminate\Support\Collection + */ + private $defaultLocation = null; + + /** + * Mapped locations collection. + * + * @var \Illuminate\Support\Collection + */ + private $mappedLocations = null; + + /** + * The summary collection. + * + * @var \Illuminate\Support\Collection + */ + private $summary; + + /** + * Is dry-run? + * + * @var bool + */ + private $dryrun = false; + + /** + * Show users to be imported. + * + * @var array + */ + private $userlist = []; /** * Create a new command instance. - * - * @return void */ - public function __construct() + public function __construct(LdapAd $ldap) { parent::__construct(); + $this->ldap = $ldap; + $this->settings = $this->ldap->ldapSettings; + $this->summary = collect(); } /** @@ -42,207 +108,271 @@ class LdapSync extends Command */ public function handle() { - ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes - ini_set('memory_limit', env('LDAP_MEM_LIM', '500M')); - $ldap_result_username = Setting::getSettings()->ldap_username_field; - $ldap_result_last_name = Setting::getSettings()->ldap_lname_field; - $ldap_result_first_name = Setting::getSettings()->ldap_fname_field; + ini_set('max_execution_time', '600'); //600 seconds = 10 minutes + ini_set('memory_limit', '500M'); - $ldap_result_active_flag = Setting::getSettings()->ldap_active_flag_field; - $ldap_result_emp_num = Setting::getSettings()->ldap_emp_num; - $ldap_result_email = Setting::getSettings()->ldap_email; - - try { - $ldapconn = Ldap::connectToLdap(); - Ldap::bindAdminToLdap($ldapconn); - } catch (\Exception $e) { - if ($this->option('json_summary')) { - $json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ]; - $this->info(json_encode($json_summary)); - } - LOG::info($e); - return []; + if ($this->option('dryrun')) { + $this->dryrun = true; + } + $this->checkIfLdapIsEnabled(); + $this->checkLdapConnetion(); + $this->setBaseDn(); + $this->getUserDefaultLocation(); + /* + * Use the default location if set, this is needed for the LDAP users sync page + */ + if (!$this->option('base_dn') && null == $this->defaultLocation) { + $this->getMappedLocations(); + } + $this->processLdapUsers(); + // Print table of users + if ($this->dryrun) { + $this->info('The following users will be synced!'); + $headers = ['First Name', 'Last Name', 'Username', 'Email', 'Employee #', 'Location Id', 'Status']; + $this->table($headers, $this->summary->toArray()); } - $summary = array(); + return $this->getSummary(); + } - try { - if ($this->option('base_dn') != '') { - $search_base = $this->option('base_dn'); - LOG::debug('Importing users from specified base DN: \"'.$search_base.'\".'); - } else { - $search_base = null; - } - $results = Ldap::findLdapUsers($search_base); - } catch (\Exception $e) { - if ($this->option('json_summary')) { - $json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ]; - $this->info(json_encode($json_summary)); - } - LOG::info($e); - return []; - } - - /* Determine which location to assign users to by default. */ - $location = NULL; - - if ($this->option('location')!='') { - $location = Location::where('name', '=', $this->option('location'))->first(); - LOG::debug('Location name '.$this->option('location').' passed'); - LOG::debug('Importing to '.$location->name.' ('.$location->id.')'); - } elseif ($this->option('location_id')!='') { - $location = Location::where('id', '=', $this->option('location_id'))->first(); - LOG::debug('Location ID '.$this->option('location_id').' passed'); - LOG::debug('Importing to '.$location->name.' ('.$location->id.')'); - } - - if (!isset($location)) { - LOG::debug('That location is invalid or a location was not provided, so no location will be assigned by default.'); - } - - /* Process locations with explicitly defined OUs, if doing a full import. */ - if ($this->option('base_dn')=='') { - // Retrieve locations with a mapped OU, and sort them from the shallowest to deepest OU (see #3993) - $ldap_ou_locations = Location::where('ldap_ou', '!=', '')->get()->toArray(); - $ldap_ou_lengths = array(); - - foreach ($ldap_ou_locations as $location) { - $ldap_ou_lengths[] = strlen($location["ldap_ou"]); - } - - array_multisort($ldap_ou_lengths, SORT_ASC, $ldap_ou_locations); - - if (sizeof($ldap_ou_locations) > 0) { - LOG::debug('Some locations have special OUs set. Locations will be automatically set for users in those OUs.'); - } - - // Inject location information fields - for ($i = 0; $i < $results["count"]; $i++) { - $results[$i]["ldap_location_override"] = false; - $results[$i]["location_id"] = 0; - } - - // Grab subsets based on location-specific DNs, and overwrite location for these users. - foreach ($ldap_ou_locations as $ldap_loc) { - $location_users = Ldap::findLdapUsers($ldap_loc["ldap_ou"]); - $usernames = array(); - for ($i = 0; $i < $location_users["count"]; $i++) { - - if (array_key_exists($ldap_result_username, $location_users[$i])) { - $location_users[$i]["ldap_location_override"] = true; - $location_users[$i]["location_id"] = $ldap_loc["id"]; - $usernames[] = $location_users[$i][$ldap_result_username][0]; - } + /** + * Generate the LDAP sync summary. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return string + */ + private function getSummary(): string + { + if ($this->option('summary') && null === $this->dryrun) { + $this->summary->each(function ($item) { + $this->info('USER: '.$item['note']); + if ('ERROR' === $item['status']) { + $this->error('ERROR: '.$item['note']); } - - // Delete located users from the general group. - foreach ($results as $key => $generic_entry) { - if ((is_array($generic_entry)) && (array_key_exists($ldap_result_username, $generic_entry))) { - if (in_array($generic_entry[$ldap_result_username][0], $usernames)) { - unset($results[$key]); - } - } - } - - $global_count = $results['count']; - $results = array_merge($location_users, $results); - $results['count'] = $global_count; - } - } - - /* Create user account entries in Snipe-IT */ - $tmp_pass = substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 20); - $pass = bcrypt($tmp_pass); - - for ($i = 0; $i < $results["count"]; $i++) { - if (empty($ldap_result_active_flag) || $results[$i][$ldap_result_active_flag][0] == "TRUE") { - - $item = array(); - $item["username"] = isset($results[$i][$ldap_result_username][0]) ? $results[$i][$ldap_result_username][0] : ""; - $item["employee_number"] = isset($results[$i][$ldap_result_emp_num][0]) ? $results[$i][$ldap_result_emp_num][0] : ""; - $item["lastname"] = isset($results[$i][$ldap_result_last_name][0]) ? $results[$i][$ldap_result_last_name][0] : ""; - $item["firstname"] = isset($results[$i][$ldap_result_first_name][0]) ? $results[$i][$ldap_result_first_name][0] : ""; - $item["email"] = isset($results[$i][$ldap_result_email][0]) ? $results[$i][$ldap_result_email][0] : "" ; - $item["ldap_location_override"] = isset($results[$i]["ldap_location_override"]) ? $results[$i]["ldap_location_override"]:""; - $item["location_id"] = isset($results[$i]["location_id"]) ? $results[$i]["location_id"]:""; - - $user = User::where('username', $item["username"])->first(); - if ($user) { - // Updating an existing user. - $item["createorupdate"] = 'updated'; - } else { - // Creating a new user. - $user = new User; - $user->password = $pass; - $user->activated = 0; - $item["createorupdate"] = 'created'; - } - - $user->first_name = $item["firstname"]; - $user->last_name = $item["lastname"]; - $user->username = $item["username"]; - $user->email = $item["email"]; - $user->employee_num = e($item["employee_number"]); - - // Sync activated state for Active Directory. - if ( array_key_exists('useraccountcontrol', $results[$i]) ) { - $enabled_accounts = [ - '512', '544', '66048', '66080', '262656', '262688', '328192', '328224', '4260352' - ]; - $user->activated = ( in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts) ) ? 1 : 0; - } - - // If we're not using AD, and there isn't an activated flag set, activate all users - elseif (empty($ldap_result_active_flag)) { - $user->activated = 1; - } - - if ($item['ldap_location_override'] == true) { - $user->location_id = $item['location_id']; - } elseif ((isset($location)) && (!empty($location))) { - - if ((is_array($location)) && (array_key_exists('id', $location))) { - $user->location_id = $location['id']; - } elseif (is_object($location)) { - $user->location_id = $location->id; - } - - } - - $user->ldap_import = 1; - - $errors = ''; - - if ($user->save()) { - $item["note"] = $item["createorupdate"]; - $item["status"]='success'; - } else { - foreach ($user->getErrors()->getMessages() as $key => $err) { - $errors .= $err[0]; - } - $item["note"] = $errors; - $item["status"]='error'; - } - - array_push($summary, $item); - } - - } - - if ($this->option('summary')) { - for ($x = 0; $x < count($summary); $x++) { - if ($summary[$x]['status']=='error') { - $this->error('ERROR: '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was not imported: '.$summary[$x]['note']); - } else { - $this->info('User '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was '.strtoupper($summary[$x]['createorupdate']).'.'); - } - } - } else if ($this->option('json_summary')) { - $json_summary = [ "error" => false, "error_message" => "", "summary" => $summary ]; + }); + } elseif ($this->option('json_summary')) { + $json_summary = [ + 'error' => false, + 'error_message' => '', + 'summary' => $this->summary->toArray(), + ]; $this->info(json_encode($json_summary)); - } else { - return $summary; + } + + return ''; + } + + /** + * Create a new user or update an existing user. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param \Adldap\Models\User $snipeUser + */ + private function updateCreateUser(AdldapUser $snipeUser): void + { + $user = $this->ldap->processUser($snipeUser, $this->defaultLocation, $this->mappedLocations); + $summary = [ + 'firstname' => $user->first_name, + 'lastname' => $user->last_name, + 'username' => $user->username, + 'employee_number' => $user->employee_num, + 'email' => $user->email, + 'location_id' => $user->location_id, + ]; + // Only update the database if is not a dry run + if (!$this->dryrun) { + if ($user->save()) { + $summary['note'] = ($user->wasRecentlyCreated ? 'CREATED' : 'UPDATED'); + $summary['status'] = 'SUCCESS'; + } else { + $errors = ''; + foreach ($user->getErrors()->getMessages() as $error) { + $errors .= $error[0]; + } + $summary['note'] = $userMsg.' was not imported. REASON: '.$errors; + $summary['status'] = 'ERROR'; + } + } + + $summary['note'] = ($user->getOriginal('username') ? 'UPDATED' : 'CREATED'); + $this->summary->push($summary); + } + + /** + * Process the users to update / create. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param int $page The page to get the result set + */ + private function processLdapUsers(int $page=0): void + { + try { + $ldapUsers = $this->ldap->getLdapUsers($page); + } catch (Exception $e) { + $this->outputError($e); + exit($e->getMessage()); + } + + if (0 == $ldapUsers->count()) { + $msg = 'ERROR: No users found!'; + Log::error($msg); + if ($this->dryrun) { + $this->error($msg); + } + exit($msg); + } + + // Process each individual users + foreach ($ldapUsers as $user) { + $this->updateCreateUser($user); + } + + if ($ldapUsers->getCurrentPage() < $ldapUsers->getPages()-1) { + $this->processLdapUsers($ldapUsers->getCurrentPage() + 1); } } + + /** + * Get the mapped locations if a base_dn is provided. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function getMappedLocations() + { + $ldapOuLocation = Location::where('ldap_ou', '!=', '')->select(['id', 'ldap_ou'])->get(); + $locations = $ldapOuLocation->sortBy(function ($ou, $key) { + return strlen($ou->ldap_ou); + }); + if ($locations->count() > 0) { + $msg = 'Some locations have special OUs set. Locations will be automatically set for users in those OUs.'; + LOG::debug($msg); + if ($this->dryrun) { + $this->info($msg); + } + + $this->mappedLocations = $locations->pluck('ldap_ou', 'id'); + } + } + + /** + * Set the base dn if supplied. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function setBaseDn(): void + { + if ($this->option('base_dn')) { + $this->ldap->baseDn = $this->option('base_dn'); + $msg = sprintf('Importing users from specified base DN: "%s"', $this->ldap->baseDn); + LOG::debug($msg); + if ($this->dryrun) { + $this->info($msg); + } + } + } + + /** + * Get a default location id for imported users. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function getUserDefaultLocation(): void + { + $location = $this->option('location_id') ?? $this->option('location'); + if ($location) { + $userLocation = Location::where('name', '=', $location) + ->orWhere('id', '=', intval($location)) + ->select(['name', 'id']) + ->first(); + if ($userLocation) { + $msg = 'Importing users with default location: '.$userLocation->name.' ('.$userLocation->id.')'; + LOG::debug($msg); + + if ($this->dryrun) { + $this->info($msg); + } + + $this->defaultLocation = collect([ + $userLocation->id => $userLocation->name, + ]); + } else { + $msg = 'The supplied location is invalid!'; + LOG::error($msg); + if ($this->dryrun) { + $this->error($msg); + } + exit(0); + } + } + } + + /** + * Check if LDAP intergration is enabled. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function checkIfLdapIsEnabled(): void + { + if (false === $this->settings['ldap_enabled']) { + $msg = 'LDAP intergration is not enabled. Exiting sync process.'; + $this->info($msg); + Log::info($msg); + exit(0); + } + } + + /** + * Check to make sure we can access the server. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function checkLdapConnetion(): void + { + try { + $this->ldap->testLdapAdUserConnection(); + $this->ldap->testLdapAdBindConnection(); + } catch (Exception $e) { + $this->outputError($e); + exit(0); + } + } + + /** + * Output the json summary to the screen if enabled. + * + * @param Exception $error + */ + private function outputError($error): void + { + if ($this->option('json_summary')) { + $json_summary = [ + 'error' => true, + 'error_message' => $error->getMessage(), + 'summary' => [], + ]; + $this->info(json_encode($json_summary)); + } + $this->error($error->getMessage()); + LOG::error($error); + } } diff --git a/app/Http/Controllers/Users/LDAPImportController.php b/app/Http/Controllers/Users/LDAPImportController.php index 6ae7178fc8..6e2a3091b0 100644 --- a/app/Http/Controllers/Users/LDAPImportController.php +++ b/app/Http/Controllers/Users/LDAPImportController.php @@ -44,7 +44,7 @@ class LDAPImportController extends Controller { $this->authorize('update', User::class); try { - $this->ldap->connect(); + //$this->ldap->connect(); I don't think this actually exists in LdapAd.php, and we don't really 'persist' LDAP connections anyways...right? } catch (\Exception $e) { return redirect()->route('users.index')->with('error', $e->getMessage()); } diff --git a/app/Providers/LdapServiceProvider.php b/app/Providers/LdapServiceProvider.php index fc1354a468..cc91bac1a5 100644 --- a/app/Providers/LdapServiceProvider.php +++ b/app/Providers/LdapServiceProvider.php @@ -13,7 +13,7 @@ class LdapServiceProvider extends ServiceProvider */ public function boot() { - $this->app->singleton(LdapAd::class, LdapAd::class); + } @@ -24,6 +24,6 @@ class LdapServiceProvider extends ServiceProvider */ public function register() { - + $this->app->singleton(LdapAd::class, LdapAd::class); } } diff --git a/app/Services/LdapAd.php b/app/Services/LdapAd.php index b7bc77148f..52414b1883 100644 --- a/app/Services/LdapAd.php +++ b/app/Services/LdapAd.php @@ -52,9 +52,9 @@ class LdapAd extends LdapAdConfiguration * * @since 5.0.0 * - * @return bool + * @return void */ - public function init() : bool + public function init() { // Already initialized if($this->ldap) { @@ -70,6 +70,10 @@ class LdapAd extends LdapAdConfiguration return false; } + public function __construct() { + $this->init(); + } + /** * Create a user if they successfully login to the LDAP server. * @@ -376,7 +380,7 @@ class LdapAd extends LdapAdConfiguration public function testLdapAdBindConnection(): void { try { - $this->ldap->search()->ous()->get()->count(); + $this->ldap->search()->ous()->get()->count(); //it's saying this is null? } catch (Exception $th) { Log::error($th->getMessage()); throw new Exception('Unable to search LDAP directory!'); @@ -393,7 +397,7 @@ class LdapAd extends LdapAdConfiguration public function testLdapAdUserConnection(): void { try { - $this->ldap->connect(); + $this->ldap->connect(); //uh, this doesn't seem to exist :/ } catch (\Adldap\Auth\BindException $e) { Log::error($e); throw new Exception('Unable to connect to LDAP directory!'); @@ -438,7 +442,7 @@ class LdapAd extends LdapAdConfiguration */ public function getLdapUsers(): Paginator { - $search = $this->ldap->search()->users()->in($this->getBaseDn()); + $search = $this->ldap->search()->users()->in($this->getBaseDn()); //this looks wrong; we should instead have a passable parameter that does this, and use this as a 'sane' default, yeah? $filter = $this->getFilter(); if (!is_null($filter)) { diff --git a/app/Services/LdapAdConfiguration.php b/app/Services/LdapAdConfiguration.php index 638dc450a5..01e9564d46 100644 --- a/app/Services/LdapAdConfiguration.php +++ b/app/Services/LdapAdConfiguration.php @@ -153,7 +153,7 @@ class LdapAdConfiguration 'password' => $this->ldapSettings['ldap_pword'], // Optional Configuration Options - 'schema' => $this->getSchema(), + 'schema' => $this->getSchema(), // FIXME - we probably ought not to be using this, right? 'account_prefix' => '', 'account_suffix' => '', 'port' => $this->getPort(), @@ -180,7 +180,7 @@ class LdapAdConfiguration * * @return string */ - private function getSchema(): string + private function getSchema(): string //wait, what? This is a little weird, since we have completely separate variables for this; we probably shoulnd't be using any 'schema' at all { $schema = \Adldap\Schemas\OpenLDAP::class; if ($this->ldapSettings['is_ad']) {