From 4dda28de9ec2d13df7ab67e9395bdaec79dfa504 Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Tue, 27 Jul 2021 20:09:58 -0700 Subject: [PATCH] WIP: cleaning up LDAP --- app/Console/Commands/LdapSyncNg.php | 398 ------------ .../Controllers/Api/SettingsController.php | 1 - .../Users/LDAPImportController.php | 23 +- app/Providers/LdapServiceProvider.php | 39 -- app/Services/LdapAd.php | 572 ------------------ app/Services/LdapAdConfiguration.php | 311 ---------- composer.json | 1 - composer.lock | 121 +--- config/app.php | 1 - 9 files changed, 3 insertions(+), 1464 deletions(-) delete mode 100644 app/Console/Commands/LdapSyncNg.php delete mode 100644 app/Providers/LdapServiceProvider.php delete mode 100644 app/Services/LdapAd.php delete mode 100644 app/Services/LdapAdConfiguration.php diff --git a/app/Console/Commands/LdapSyncNg.php b/app/Console/Commands/LdapSyncNg.php deleted file mode 100644 index b271c6c10c..0000000000 --- a/app/Console/Commands/LdapSyncNg.php +++ /dev/null @@ -1,398 +0,0 @@ - - * - * @since 5.0.0 - */ -class LdapSyncNg extends Command -{ - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'snipeit:ldap-sync-ng - {--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/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. - */ - public function __construct(LdapAd $ldap) - { - parent::__construct(); - $this->ldap = $ldap; - $this->settings = $this->ldap->ldapSettings; - $this->summary = collect(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $dispatcher = \Adldap\Adldap::getEventDispatcher(); - - // Listen for all model events. - $dispatcher->listen('Adldap\Models\Events\*', function ($eventName, array $data) { - echo $eventName; // Returns 'Adldap\Models\Events\Updating' - var_dump($data); // Returns [0] => (object) Adldap\Models\Events\Updating; - \Log::debug('Event: '.$eventName.' data - '.print_r($data, true)); - }); - $dispatcher->listen('Adldap\Auth\Events\*', function ($eventName, array $data) { - echo $eventName; // Returns 'Adldap\Models\Events\Updating' - var_dump($data); // Returns [0] => (object) Adldap\Models\Events\Updating; - \Log::debug('Event: '.$eventName.' data - '.print_r($data, true)); - }); - - ini_set('max_execution_time', env('LDAP_TIME_LIM', '600')); //600 seconds = 10 minutes - ini_set('memory_limit', '500M'); - $old_error_reporting = error_reporting(); // grab old error_reporting .ini setting, for later re-enablement - error_reporting($old_error_reporting & ~E_DEPRECATED); // disable deprecation warnings, for LDAP in PHP 7.4 (and greater) - - if ($this->option('dryrun')) { - $this->dryrun = true; - } - $this->checkIfLdapIsEnabled(); - $this->checkLdapConnection(); - $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()); - } - - error_reporting($old_error_reporting); // re-enable deprecation warnings. - - return $this->getSummary(); - } - - /** - * 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']); - } - }); - } elseif ($this->option('json_summary')) { - $json_summary = [ - 'error' => false, - 'error_message' => '', - 'summary' => $this->summary->toArray(), - ]; - $this->info(json_encode($json_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->isDirty()) { //if nothing on the user changed, don't bother trying to save anything nor put anything in the summary - if ($user->save()) { - $summary['note'] = ($user->wasRecentlyCreated ? 'CREATED' : 'UPDATED'); - $summary['status'] = 'SUCCESS'; - } else { - $errors = ''; - foreach ($user->getErrors()->getMessages() as $error) { - $errors .= implode(', ', $error); - } - $summary['note'] = $snipeUser->getDN().' was not imported. REASON: '.$errors; - $summary['status'] = 'ERROR'; - } - } else { - $summary = null; - } - } - - // $summary['note'] = ($user->getOriginal('username') ? 'UPDATED' : 'CREATED'); // this seems, kinda, like, superfluous, relative to the $summary['note'] thing above, yeah? - if ($summary) { //if the $user wasn't dirty, $summary was set to null so that we will skip the following push() - $this->summary->push($summary); - } - } - - /** - * Process the users to update / create. - * - * @author Wes Hulette - * - * @since 5.0.0 - */ - private function processLdapUsers(): void - { - try { - \Log::debug('CAL:LING GET LDAP SUSERS'); - $ldapUsers = $this->ldap->getLdapUsers(); - \Log::debug('END CALLING GET LDAP USERS'); - } 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->getResults() as $user) { // AdLdap2's paginate() method is weird, it gets *everything* and ->getResults() returns *everything* - $this->updateCreateUser($user); - } - } - - /** - * 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'); // TODO: this seems ok-ish, but the key-> value is going location_id -> OU name, and the primary action here is the opposite of that - going from OU's to location ID's. - } - } - - /** - * 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 checkLdapConnection(): 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/Api/SettingsController.php b/app/Http/Controllers/Api/SettingsController.php index 852530c207..59236731e4 100644 --- a/app/Http/Controllers/Api/SettingsController.php +++ b/app/Http/Controllers/Api/SettingsController.php @@ -7,7 +7,6 @@ use App\Http\Transformers\LoginAttemptsTransformer; use App\Models\Ldap; use App\Models\Setting; use App\Notifications\MailTest; -use App\Services\LdapAd; use GuzzleHttp\Client; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/app/Http/Controllers/Users/LDAPImportController.php b/app/Http/Controllers/Users/LDAPImportController.php index a204581eb6..88a6b207df 100644 --- a/app/Http/Controllers/Users/LDAPImportController.php +++ b/app/Http/Controllers/Users/LDAPImportController.php @@ -4,32 +4,12 @@ namespace App\Http\Controllers\Users; use App\Http\Controllers\Controller; use App\Models\User; -use App\Services\LdapAd; use Illuminate\Http\Request; use Illuminate\Support\Facades\Artisan; // Note that this is awful close to 'Users' the namespace above; be careful class LDAPImportController extends Controller { - /** - * An Ldap instance. - * - * @var LdapAd - */ - protected $ldap; - - /** - * __construct. - * - * @param LdapAd $ldap - */ - public function __construct(LdapAd $ldap) - { - parent::__construct(); - $this->ldap = $ldap; - $this->ldap->init(); - } - - /** + /** * Return view for LDAP import. * * @author Aladin Alaily @@ -43,6 +23,7 @@ class LDAPImportController extends Controller */ public function create() { + // I guess this prolly oughtta... I dunno. Do something? $this->authorize('update', User::class); try { //$this->ldap->connect(); I don't think this actually exists in LdapAd.php, and we don't really 'persist' LDAP connections anyways...right? diff --git a/app/Providers/LdapServiceProvider.php b/app/Providers/LdapServiceProvider.php deleted file mode 100644 index 58f93096d3..0000000000 --- a/app/Providers/LdapServiceProvider.php +++ /dev/null @@ -1,39 +0,0 @@ -app->singleton(LdapAd::class, LdapAd::class); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return [LdapAd::class]; - } -} diff --git a/app/Services/LdapAd.php b/app/Services/LdapAd.php deleted file mode 100644 index 186a2c7a9d..0000000000 --- a/app/Services/LdapAd.php +++ /dev/null @@ -1,572 +0,0 @@ - - * - * @since 5.0.0 - */ -class LdapAd extends LdapAdConfiguration -{ - /* The following is _probably_ the correct logic, but we can't use it because - some users may have been dependent upon the previous behavior, and this - could cause additional access to be available to users they don't want - to allow to log in. - $useraccountcontrol = $results[$i]['useraccountcontrol'][0]; - if( - // based on MS docs at: https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties - ($useraccountcontrol & 0x200) && // is a NORMAL_ACCOUNT - !($useraccountcontrol & 0x02) && // *and* _not_ ACCOUNTDISABLE - !($useraccountcontrol & 0x10) // *and* _not_ LOCKOUT - ) { - $user->activated = 1; - } else { - $user->activated = 0; - } */ - const AD_USER_ACCOUNT_CONTROL_FLAGS = [ - '512', // 0x200 NORMAL_ACCOUNT - '544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD - '66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD - '66080', // 0x10220 NORMAL_ACCOUNT, PASSWD_NOTREQD, DONT_EXPIRE_PASSWORD - '262656', // 0x40200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED - '262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED - '328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD - '328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD - '4260352', // 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH - '1049088', // 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED - '1114624', // 0x110200 NORMAL_ACCOUNT, NOT_DELEGATED, DONT_EXPIRE_PASSWORD - ]; - - /** - * The LDAP results per page. - */ - const PAGE_SIZE = 500; - - /** - * A base dn. - * - * @var string - */ - public $baseDn = null; - - /** - * Adldap instance. - * - * @var \Adldap\Adldap - */ - protected $ldap; - - /** - * Initialize LDAP from user settings - * - * @since 5.0.0 - * - * @return void - */ - public function init() - { - // Already initialized - if ($this->ldap) { - return true; - } - - parent::init(); - if ($this->isLdapEnabled()) { - if ($this->ldapSettings['is_ad'] == 0) { //only for NON-AD setups! - $this->ldapConfig['account_prefix'] = $this->ldapSettings['ldap_auth_filter_query']; - $this->ldapConfig['account_suffix'] = ','.$this->ldapConfig['base_dn']; - } /* - To the point mentioned in ldapLogin(), we might want to add an 'else' clause here that - sets up an 'account_suffix' of '@'.$this->ldapSettings['ad_domain'] *IF* the user has - $this->ldapSettings['ad_append_domain'] enabled. - That code in ldapLogin gets simplified, in exchange for putting all the weirdness here only. - */ - $this->ldap = new Adldap(); - $this->ldap->addProvider($this->ldapConfig); - - return true; - } - - return false; - } - - public function __construct() - { - $this->init(); - } - - /** - * Create a user if they successfully login to the LDAP server. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @param string $username - * @param string $password - * - * @return \App\Models\User - * - * @throws Exception - */ - public function ldapLogin(string $username, string $password): User - { - if ($this->ldapSettings['ad_append_domain']) { //if you're using 'userprincipalname', don't check the ad_append_domain checkbox - $login_username = $username.'@'.$this->ldapSettings['ad_domain']; // I feel like could can be solved with the 'suffix' feature? Then this would be easier. - } else { - $login_username = $username; - } - - if ($this->ldapConfig['username'] && $this->ldapConfig['password']) { - $bind_as_user = false; - } else { - $bind_as_user = true; - } - - if (($this->ldap) && ($this->ldap->auth()->attempt($login_username, $password, $bind_as_user) === false)) { - throw new Exception('Unable to validate user credentials!'); - } - - // Should we sync the logged in user - Log::debug('Attempting to find user in LDAP directory'); - $record = $this->ldap->search()->findBy($this->ldapSettings['ldap_username_field'], $username); - - if ($record) { - if ($this->isLdapSync($record)) { - $this->syncUserLdapLogin($record, $password); - } - } else { - throw new Exception('Unable to find user in LDAP directory!'); - } - - $user = User::where('username', $username) - ->whereNull('deleted_at')->where('ldap_import', '=', 1) - ->where('activated', '=', '1')->first(); - /* Above, I could've just done ->firstOrFail() which would've been cleaner, but it would've been miserable to - troubleshoot if it ever came up (giving a really generic and untraceable error message) - */ - if (! $user) { - throw new Exception("User is either deleted, not activated (can't log in), not from LDAP, or can't be found in database"); - } - - return $user; - } - - /** - * Set the user information based on the LDAP settings. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @param \Adldap\Models\User $user - * @param null|Collection $defaultLocation - * @param null|Collection $mappedLocations - * - * @return null|\App\Models\User - */ - public function processUser(AdldapUser $user, ?Collection $defaultLocation = null, ?Collection $mappedLocations = null): ?User - { - // Only sync active users <- I think this actually means 'existing', not 'activated/deactivated' - if (! $user) { - return null; - } - $snipeUser = []; - $snipeUser['username'] = $user->{$this->ldapSettings['ldap_username_field']}[0] ?? ''; - $snipeUser['employee_number'] = $user->{$this->ldapSettings['ldap_emp_num']}[0] ?? ''; - $snipeUser['lastname'] = $user->{$this->ldapSettings['ldap_lname_field']}[0] ?? ''; - $snipeUser['firstname'] = $user->{$this->ldapSettings['ldap_fname_field']}[0] ?? ''; - $snipeUser['email'] = $user->{$this->ldapSettings['ldap_email']}[0] ?? ''; - $snipeUser['title'] = $user->getTitle() ?? ''; - $snipeUser['telephonenumber'] = $user->getTelephoneNumber() ?? ''; - - /* - * $locationId being 'null' means we have no per-OU location information, - * but instead of explicitly setting it to null - which would override any admin-generated - * location assignments - we just don't set it at all. For a brand new User, the 'default null' - * on the column will cover us. For an already existing user, this will not override any - * locations that were explicitly chosen by the administrators. - * - * When syncing with a particular 'default location' in mind, those should still be respected - * and it *will* override the administrators previous choices. I think this is a fair compromise. - */ - $locationId = $this->getLocationId($user, $defaultLocation, $mappedLocations); - if ($locationId !== null) { - $snipeUser['location_id'] = $locationId; - } - - $activeStatus = $this->getActiveStatus($user); - if ($activeStatus !== null) { - $snipeUser['activated'] = $activeStatus; - } - - return $this->setUserModel($snipeUser); - } - - /** - * Set the User model information. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @param array $userInfo The user info to save to the database - * - * @return \App\Models\User - */ - public function setUserModel(array $userInfo): User - { - // If the username exists, return the user object, otherwise create a new user object - $user = User::firstOrNew([ - 'username' => $userInfo['username'], - ]); - $user->username = $user->username ?? trim($userInfo['username']); - $user->password = $user->password ?? Helper::generateEncyrptedPassword(); - $user->first_name = trim($userInfo['firstname']); - $user->last_name = trim($userInfo['lastname']); - $user->email = trim($userInfo['email']); - $user->employee_num = trim($userInfo['employee_number']); - $user->jobtitle = trim($userInfo['title']); - $user->phone = trim($userInfo['telephonenumber']); - if (array_key_exists('activated', $userInfo)) { - $user->activated = $userInfo['activated']; - } elseif (! $user->exists) { // no 'activated' flag was set or unset, *AND* this user is new - activate by default. - $user->activated = 1; - } - if (array_key_exists('location_id', $userInfo)) { - $user->location_id = $userInfo['location_id']; - } - - // this is a new user - if (! isset($user->id)) { - $user->notes = 'Imported from LDAP'; - } - - $user->ldap_import = 1; - - return $user; - } - - /** - * Sync a user who has logged in by LDAP. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @param \Adldap\Models\User $record - * @param string $password - * - * @throws Exception - */ - private function syncUserLdapLogin(AdldapUser $record, string $password): void - { - $user = $this->processUser($record); - - if (is_null($user->last_login)) { - $user->notes = 'Imported on first login from LDAP2'; - } - - if ($this->ldapSettings['ldap_pw_sync']) { - Log::debug('Syncing users password with LDAP directory.'); - $user->password = bcrypt($password); - } - - if (! $user->save()) { - Log::debug('Could not save user. '.$user->getErrors()); - throw new Exception('Could not save user: '.$user->getErrors()); - } - } - - /** - * Check to see if we should sync the user with the LDAP directory. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @param \Adldap\Models\User $user - * - * @return bool - */ - private function isLdapSync(AdldapUser $user): bool - { - if (! $this->ldapSettings['ldap_active_flag']) { - return true; // always sync if you didn't define an 'active' flag - } - - if ($user->{$this->ldapSettings['ldap_active_flag']} && // if your LDAP user has the aforementioned flag as an attribute *AND* - count($user->{$this->ldapSettings['ldap_active_flag']}) == 1 && // if that attribute has exactly one value *AND* - strtolower($user->{$this->ldapSettings['ldap_active_flag']}[0]) == 'false') { // that value is the string 'false' (regardless of case), - return false; // then your user is *INACTIVE* - return false - } - // otherwise, return true - return true; - } - - /** - * Set the active status of the user. - * Returns 0 or 1 if the user is deactivated or activated - * or returns null if we just don't know - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @param \Adldap\Models\User $user - * - * @return int (or null) - */ - private function getActiveStatus(AdldapUser $user): ?int - { - /* - * Check to see if we are connected to an AD server - * if so, check the Active Directory User Account Control Flags - * If the admin has set their own 'active flag' - respect that instead - * (this may work to allow AD users to ignore the built-in UAC stuff that AD does) - */ - if ($user->hasAttribute($user->getSchema()->userAccountControl()) && ! $this->ldapSettings['ldap_active_flag']) { - \Log::debug('This is AD - userAccountControl is'.$user->getSchema()->userAccountControl()); - $activeStatus = (in_array($user->getUserAccountControl(), self::AD_USER_ACCOUNT_CONTROL_FLAGS)) ? 1 : 0; - } else { - - //\Log::debug('This looks like LDAP (or an AD where the UAC is disabled)'); - // If there is no activated flag, then we can't make any determination about activated/deactivated - if (false == $this->ldapSettings['ldap_active_flag']) { - \Log::debug('ldap_active_flag is false - no ldap_active_flag is set'); - - return null; - } - - // If there *is* an activated flag, then respect it *only* if it is actually present. If it's not there, ignore it. - if (! $user->hasAttribute($this->ldapSettings['ldap_active_flag'])) { - return null; // 'active' flag is defined, but does not exist on returned user record. So we don't know if they're active or not. - } - - // if $user has the flag *AND* that flag has exactly one value - - if ($user->{$this->ldapSettings['ldap_active_flag']} && count($user->{$this->ldapSettings['ldap_active_flag']}) == 1) { - $active_flag_value = $user->{$this->ldapSettings['ldap_active_flag']}[0]; - - // if the value of that flag is case-insensitively the string 'false' or boolean false - if (strcasecmp($active_flag_value, 'false') == 0 || $active_flag_value === false) { - return 0; // then make them INACTIVE - } else { - return 1; // otherwise active - } - } - - return 1; // fail 'open' (active) if we have the attribute and it's multivalued or empty; that's weird - } - - return $activeStatus; - } - - /** - * Get a default selected location, or a OU mapped location if available. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @param \Adldap\Models\User $user - * @param Collection|null $defaultLocation - * @param Collection|null $mappedLocations - * - * @return null|int - */ - private function getLocationId(AdldapUser $user, ?Collection $defaultLocation, ?Collection $mappedLocations): ?int - { - $locationId = null; - // Set the users default locations, if set - if ($defaultLocation) { - $locationId = $defaultLocation->keys()->first(); - } - - // Check to see if the user is in a mapped location - if ($mappedLocations) { - $location = $mappedLocations->filter(function ($value, $key) use ($user) { - //if ($user->inOu($value)) { // <----- *THIS* seems not to be working, and it seems more 'intelligent' - but it's literally just a strpos() call, and it doesn't work quite right against plain strings - $user_ou = substr($user->getDn(), -strlen($value)); // get the LAST chars of the user's DN, the count of those chars being the length of the thing we're checking against - if (strcasecmp($user_ou, $value) === 0) { // case *IN*sensitive comparision - some people say OU=blah, some say ou=blah. returns 0 when strings are identical (which is a little odd, yeah) - return $key; // WARNING: we are doing a 'filter' - not a regular for-loop. So the answer(s) get "return"ed into the $location array - } - }); - - if ($location->count() > 0) { - $locationId = $location->keys()->first(); // from the returned $location array from the ->filter() method above, we return the first match - there should be only one - } - } - - return $locationId; - } - - /** - * Get the base dn for the query. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return string - */ - private function getBaseDn(): string - { - if (! is_null($this->baseDn)) { - return $this->baseDn; - } - - return $this->ldapSettings['ldap_basedn']; - } - - /** - * Format the ldap filter if needed. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return null|string - */ - private function getFilter(): ?string - { - $filter = $this->ldapSettings['ldap_filter']; - if (! $filter) { - return null; - } - // Add surrounding parentheses as needed - $paren = mb_substr($filter, 0, 1, 'utf-8'); - if ('(' !== $paren) { - return '('.$filter.')'; - } - - return $filter; - } - - /** - * Get the selected fields to return - * This should help with memory on large result sets as we are not returning all fields. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return array - */ - private function getSelectedFields(): array - { - /** @var Schema $schema */ - $schema = new $this->ldapConfig['schema']; - - return array_values(array_filter([ - $this->ldapSettings['ldap_username_field'], - $this->ldapSettings['ldap_fname_field'], - $this->ldapSettings['ldap_lname_field'], - $this->ldapSettings['ldap_email'], - $this->ldapSettings['ldap_emp_num'], - $this->ldapSettings['ldap_active_flag'], - $schema->memberOf(), - $schema->userAccountControl(), - $schema->title(), - $schema->telephone(), - ])); - } - - /** - * Test the bind user connection. - * - * @author Wes Hulette - * @throws \Exception - * @since 5.0.0 - */ - public function testLdapAdBindConnection(): void - { - try { - $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!'); - } - } - - /** - * Test the user can connect to the LDAP server. - * - * @author Wes Hulette - * @throws \Exception - * @since 5.0.0 - */ - public function testLdapAdUserConnection(): void - { - try { - $this->ldap->connect(); - } catch (\Exception $e) { - Log::debug('LDAP ERROR: '.$e->getMessage()); - throw new Exception($e->getMessage()); - } - } - - /** - * Test the LDAP configuration by returning up to 10 users. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return Collection - */ - public function testUserImportSync(): Collection - { - $testUsers = collect($this->getLdapUsers()->getResults())->chunk(10)->first(); - if ($testUsers) { - return $testUsers->map(function ($item) { - return (object) [ - 'username' => $item->{$this->ldapSettings['ldap_username_field']}[0] ?? null, - 'employee_number' => $item->{$this->ldapSettings['ldap_emp_num']}[0] ?? null, - 'lastname' => $item->{$this->ldapSettings['ldap_lname_field']}[0] ?? null, - 'firstname' => $item->{$this->ldapSettings['ldap_fname_field']}[0] ?? null, - 'email' => $item->{$this->ldapSettings['ldap_email']}[0] ?? null, - ]; - }); - } - - return collect(); - } - - /** - * Query the LDAP server to get the users to process and return a page set. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return \Adldap\Query\Paginator - */ - public function getLdapUsers(): Paginator - { - $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)) { - $search = $search->rawFilter($filter); - } - //I think it might be possible to potentially do our own paging here? - - return $search->select($this->getSelectedFields()) - ->paginate(self::PAGE_SIZE); - } -} diff --git a/app/Services/LdapAdConfiguration.php b/app/Services/LdapAdConfiguration.php deleted file mode 100644 index 25f0fba372..0000000000 --- a/app/Services/LdapAdConfiguration.php +++ /dev/null @@ -1,311 +0,0 @@ - - * - * @since 5.0.0 - */ -class LdapAdConfiguration -{ - const LDAP_PORT = 389; - const CONNECTION_TIMEOUT = 5; - const DEFAULT_LDAP_VERSION = 3; - const LDAP_BOOLEAN_SETTINGS = [ - 'ldap_enabled', - 'ldap_server_cert_ignore', - 'ldap_tls', - 'ldap_tls', - 'ldap_pw_sync', - 'is_ad', - 'ad_append_domain', - ]; - - /** - * Ldap Settings. - * - * @var Collection - */ - public $ldapSettings; - - /** - * LDAP Config. - * - * @var array - */ - public $ldapConfig; - - /** - * Initialize LDAP from user settings - * - * @since 5.0.0 - */ - public function init() - { - - // This try/catch is dumb, but is necessary to run initial migrations, since - // this service provider is booted even during migrations. :( - snipe - try { - $this->ldapSettings = $this->getSnipeItLdapSettings(); - if ($this->isLdapEnabled()) { - $this->setSnipeItConfig(); - } - } catch (\Exception $e) { - \Log::debug($e); - $this->ldapSettings = null; - } - } - - /** - * Merge the default Adlap config with the SnipeIT config. - * - * @author Wes Hulette - * - * @since 5.0.0 - */ - private function setSnipeItConfig() - { - $this->ldapConfig = $this->setLdapConnectionConfiguration(); - $this->certificateCheck(); - } - - /** - * Get the LDAP settings from the Settings model. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return \Illuminate\Support\Collection - */ - private function getSnipeItLdapSettings(): Collection - { - $ldapSettings = collect(); - if (Setting::first()) { // during early migration steps, there may be no settings table entry to start with - $ldapSettings = Setting::getLdapSettings() - ->map(function ($item, $key) { - // Trim the items - if (is_string($item)) { - $item = trim($item); - } - // Get the boolean value of the LDAP setting, makes it easier to work with them - if (in_array($key, self::LDAP_BOOLEAN_SETTINGS)) { - return boolval($item); - } - - // Decrypt the admin password - if ('ldap_pword' === $key && ! empty($item)) { - try { - return decrypt($item); - } catch (Exception $e) { - throw new Exception('Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.'); - } - } - - if ($item && 'ldap_server' === $key) { - return collect(parse_url($item)); - } - - return $item; - }); - } - - return $ldapSettings; - } - - /** - * Set the server certificate environment variable. - * - * @author Wes Hulette - * - * @since 5.0.0 - */ - private function certificateCheck(): void - { - // If we are ignoring the SSL cert we need to setup the environment variable - // before we create the connection - if ($this->ldapSettings['ldap_server_cert_ignore']) { - putenv('LDAPTLS_REQCERT=never'); - } - - // If the user specifies where CA Certs are, make sure to use them - if (env('LDAPTLS_CACERT')) { - putenv('LDAPTLS_CACERT='.env('LDAPTLS_CACERT')); - } - } - - /** - * Set the Adlap2 connection configuration values based on SnipeIT settings. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return array - */ - private function setLdapConnectionConfiguration(): array - { - // Create the configuration array. - $ldap_settings = [ - // Mandatory Configuration Options - 'hosts' => $this->getServerUrlBase(), - 'base_dn' => $this->ldapSettings['ldap_basedn'], - 'username' => $this->ldapSettings['ldap_uname'], - 'password' => $this->ldapSettings['ldap_pword'], - - // Optional Configuration Options - 'schema' => $this->getSchema(), // FIXME - we probably ought not to be using this, right? - 'account_prefix' => '', - 'account_suffix' => '', - 'port' => $this->getPort(), - 'follow_referrals' => false, - 'use_ssl' => $this->isSsl(), - 'use_tls' => $this->ldapSettings['ldap_tls'], - 'version' => $this->ldapSettings['ldap_version'] ?? self::DEFAULT_LDAP_VERSION, - 'timeout' => self::CONNECTION_TIMEOUT, - - // Custom LDAP Options - 'custom_options' => [ - // See: http://php.net/ldap_set_option - // LDAP_OPT_X_TLS_REQUIRE_CERT => LDAP_OPT_X_TLS_HARD, - ], - ]; - - if($this->ldapSettings['ldap_client_tls_cert'] || $this->ldapSettings['ldap_client_tls_key']) { - $ldap_settings['custom_options'] = [ - LDAP_OPT_X_TLS_CERTFILE => Setting::get_client_side_cert_path(), - LDAP_OPT_X_TLS_KEYFILE => Setting::get_client_side_key_path() - ]; - } - return $ldap_settings; - } - - /** - * Get the schema to use for the connection. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return 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']) { - $schema = \Adldap\Schemas\ActiveDirectory::class; - } - - return $schema; - } - - /** - * Get the port number from the connection url. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return int - */ - private function getPort(): int - { - $port = $this->getLdapServerData('port'); - if ($port && is_int($port)) { - return $port; - } - - return self::LDAP_PORT; - } - - /** - * Get ldap scheme from url to determin ssl use. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return bool - */ - private function isSsl(): bool - { - $scheme = $this->getLdapServerData('scheme'); - if ($scheme && 'ldaps' === strtolower($scheme)) { - return true; - } - - return false; - } - - /** - * Return the base url to the LDAP server. - * - * @author Wes Hulette - * - * @since 5.0.0 - * - * @return array - */ - private function getServerUrlBase(): array - { - /* if ($this->ldapSettings['is_ad']) { - return collect(explode(',', $this->ldapSettings['ad_domain']))->map(function ($item) { - return trim($item); - })->toArray(); - } */ // <- this was the *original* intent of the PR for AdLdap2, but we've been moving away from having - // two separate fields - one for "ldap_host" and one for "ad_domain" - towards just using "ldap_host" - // ad_domain for us just means "append this domain to your usernames for login, if you click that checkbox" - // that's all, nothing more (I hope). - - $url = $this->getLdapServerData('host'); - - return $url ? [$url] : []; - } - - /** - * Get ldap enabled setting - * - * @author Steffen Buehl - * - * @since 5.0.0 - * - * @return bool - */ - public function isLdapEnabled(): bool - { - return $this->ldapSettings && $this->ldapSettings->get('ldap_enabled'); - } - - /** - * Get parsed ldap server information - * - * @author Steffen Buehl - * - * @since 5.0.0 - * - * @param $key - * @return mixed|null - */ - protected function getLdapServerData($key) - { - if ($this->ldapSettings) { - $ldapServer = $this->ldapSettings->get('ldap_server'); - if ($ldapServer && $ldapServer instanceof Collection) { - return $ldapServer->get($key); - } - } - - return null; - } -} diff --git a/composer.json b/composer.json index e27a0cd1e9..ee500769a2 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ "ext-json": "*", "ext-mbstring": "*", "ext-pdo": "*", - "adldap2/adldap2": "^10.2", "alek13/slack": "^2.0", "bacon/bacon-qr-code": "^1.0", "barryvdh/laravel-debugbar": "^3.6", diff --git a/composer.lock b/composer.lock index aa4d99d39b..3dce273326 100644 --- a/composer.lock +++ b/composer.lock @@ -4,73 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "64dee2cbecb92c4250bb11ffe23f2ed0", + "content-hash": "f67d64a4e9f3ea8ce9c340527e498d97", "packages": [ - { - "name": "adldap2/adldap2", - "version": "v10.3.3", - "source": { - "type": "git", - "url": "https://github.com/Adldap2/Adldap2.git", - "reference": "c2a8f72455d3438377d955fc0f4b9ed836b47463" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Adldap2/Adldap2/zipball/c2a8f72455d3438377d955fc0f4b9ed836b47463", - "reference": "c2a8f72455d3438377d955fc0f4b9ed836b47463", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-ldap": "*", - "illuminate/contracts": "~5.0|~6.0|~7.0|~8.0", - "php": ">=7.0", - "psr/log": "~1.0", - "psr/simple-cache": "~1.0", - "tightenco/collect": "~5.0|~6.0|~7.0|~8.0" - }, - "require-dev": { - "mockery/mockery": "~1.0", - "phpunit/phpunit": "~6.0|~7.0|~8.0" - }, - "suggest": { - "ext-fileinfo": "fileinfo is required when retrieving user encoded thumbnails" - }, - "type": "library", - "autoload": { - "psr-4": { - "Adldap\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Steve Bauman", - "email": "steven_bauman@outlook.com", - "role": "Developer" - } - ], - "description": "A PHP LDAP Package for humans.", - "keywords": [ - "active directory", - "ad", - "adLDAP", - "adldap2", - "directory", - "ldap", - "windows" - ], - "support": { - "docs": "https://github.com/Adldap2/Adldap2/blob/master/readme.md", - "email": "steven_bauman@outlook.com", - "issues": "https://github.com/Adldap2/Adldap2/issues", - "source": "https://github.com/Adldap2/Adldap2" - }, - "time": "2021-08-09T15:22:35+00:00" - }, { "name": "alek13/slack", "version": "2.1.1", @@ -10401,60 +10336,6 @@ ], "time": "2021-02-07T13:13:45+00:00" }, - { - "name": "tightenco/collect", - "version": "v8.34.0", - "source": { - "type": "git", - "url": "https://github.com/tighten/collect.git", - "reference": "b069783ab0c547bb894ebcf8e7f6024bb401f9d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/tighten/collect/zipball/b069783ab0c547bb894ebcf8e7f6024bb401f9d2", - "reference": "b069783ab0c547bb894ebcf8e7f6024bb401f9d2", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0", - "symfony/var-dumper": "^3.4 || ^4.0 || ^5.0" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "nesbot/carbon": "^2.23.0", - "phpunit/phpunit": "^8.3" - }, - "type": "library", - "autoload": { - "files": [ - "src/Collect/Support/helpers.php", - "src/Collect/Support/alias.php" - ], - "psr-4": { - "Tightenco\\Collect\\": "src/Collect" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com" - } - ], - "description": "Collect - Illuminate Collections as a separate package.", - "keywords": [ - "collection", - "laravel" - ], - "support": { - "issues": "https://github.com/tighten/collect/issues", - "source": "https://github.com/tighten/collect/tree/v8.34.0" - }, - "time": "2021-03-29T21:29:00+00:00" - }, { "name": "tightenco/ziggy", "version": "v1.2.0", diff --git a/config/app.php b/config/app.php index e8d1ebae49..ab6d62004a 100755 --- a/config/app.php +++ b/config/app.php @@ -345,7 +345,6 @@ return [ * Custom service provider */ App\Providers\MacroServiceProvider::class, - App\Providers\LdapServiceProvider::class, App\Providers\SamlServiceProvider::class, ],