mirror of
https://github.com/snipe/snipe-it.git
synced 2024-12-25 13:44:06 -08:00
Merge pull request #10265 from uberbrady/remove_old_ldap
Remove old ldap
This commit is contained in:
commit
b98c97b00e
|
@ -91,7 +91,7 @@ class LdapSync extends Command
|
|||
}
|
||||
|
||||
/* Determine which location to assign users to by default. */
|
||||
$location = null; // FIXME - this would be better called "$default_location", which is more explicit about its purpose
|
||||
$location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose
|
||||
|
||||
if ($this->option('location') != '') {
|
||||
$location = Location::where('name', '=', $this->option('location'))->first();
|
||||
|
@ -133,7 +133,7 @@ class LdapSync extends Command
|
|||
foreach ($ldap_ou_locations as $ldap_loc) {
|
||||
try {
|
||||
$location_users = Ldap::findLdapUsers($ldap_loc['ldap_ou']);
|
||||
} catch (\Exception $e) { // FIXME: this is stolen from line 77 or so above
|
||||
} catch (\Exception $e) { // TODO: this is stolen from line 77 or so above
|
||||
if ($this->option('json_summary')) {
|
||||
$json_summary = ['error' => true, 'error_message' => trans('admin/users/message.error.ldap_could_not_search').' Location: '.$ldap_loc['name'].' (ID: '.$ldap_loc['id'].') cannot connect to "'.$ldap_loc['ldap_ou'].'" - '.$e->getMessage(), 'summary' => []];
|
||||
$this->info(json_encode($json_summary));
|
||||
|
|
|
@ -1,398 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Adldap\Models\User as AdldapUser;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use App\Services\LdapAd;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* LDAP / AD sync command.
|
||||
*
|
||||
* @author Wes Hulette <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\LoginAttemptsTransformer;
|
||||
use App\Models\Ldap;
|
||||
use App\Models\Setting;
|
||||
use Mail;
|
||||
use App\Notifications\SlackTest;
|
||||
use App\Notifications\MailTest;
|
||||
use App\Services\LdapAd;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
@ -22,105 +22,69 @@ use App\Http\Requests\SlackSettingsRequest;
|
|||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Test the ldap settings
|
||||
*
|
||||
* @author Wes Hulette <jwhulette@gmail.com>
|
||||
*
|
||||
* @since 5.0.0
|
||||
*
|
||||
* @param App\Models\LdapAd $ldap
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function ldapAdSettingsTest(LdapAd $ldap): JsonResponse
|
||||
{
|
||||
if (! $ldap->init()) {
|
||||
Log::info('LDAP is not enabled so we cannot test.');
|
||||
|
||||
|
||||
public function ldaptest()
|
||||
{
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
if ($settings->ldap_enabled!='1') {
|
||||
\Log::debug('LDAP is not enabled cannot test.');
|
||||
return response()->json(['message' => 'LDAP is not enabled, cannot test.'], 400);
|
||||
}
|
||||
|
||||
// The connect, bind and resulting users message
|
||||
$message = [];
|
||||
\Log::debug('Preparing to test LDAP connection');
|
||||
|
||||
// This is all kinda fucked right now. The connection test doesn't actually do what you think,
|
||||
// // and the way we parse the errors
|
||||
// on the JS side is horrible.
|
||||
Log::info('Preparing to test LDAP user login');
|
||||
// Test user can connect to the LDAP server
|
||||
$message = []; //where we collect together test messages
|
||||
try {
|
||||
$ldap->testLdapAdUserConnection();
|
||||
$message['login'] = [
|
||||
'message' => 'Successfully connected to LDAP server.',
|
||||
];
|
||||
} catch (\Exception $ex) {
|
||||
\Log::debug('Connection to LDAP server '.Setting::getSettings()->ldap_server.' failed. Please check your LDAP settings and try again. Server Responded with error: '.$ex->getMessage());
|
||||
|
||||
return response()->json(
|
||||
['message' => 'Connection to LDAP server '.Setting::getSettings()->ldap_server." failed. Verify that the LDAP hostname is entered correctly and that it can be reached from this web server. \n\nServer Responded with error: ".$ex->getMessage(),
|
||||
|
||||
], 400);
|
||||
}
|
||||
|
||||
Log::info('Preparing to test LDAP bind connection');
|
||||
// Test user can bind to the LDAP server
|
||||
try {
|
||||
Log::info('Testing Bind');
|
||||
$ldap->testLdapAdBindConnection();
|
||||
$message['bind'] = [
|
||||
'message' => 'Successfully bound to LDAP server.',
|
||||
];
|
||||
} catch (\Exception $ex) {
|
||||
Log::info('LDAP Bind failed');
|
||||
|
||||
return response()->json(['message' => 'Connection to LDAP successful, but we were unable to Bind the LDAP user '.Setting::getSettings()->ldap_uname.". Verify your that your LDAP Bind username and password are correct. \n\nServer Responded with error: ".$ex->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
|
||||
|
||||
Log::info('Preparing to get sample user set from LDAP directory');
|
||||
// Get a sample of 10 users so user can verify the data is correct
|
||||
$settings = Setting::getSettings();
|
||||
try {
|
||||
Log::info('Testing LDAP sync');
|
||||
error_reporting(E_ALL & ~E_DEPRECATED); // workaround for php7.4, which deprecates ldap_control_paged_result
|
||||
// $users = $ldap->testUserImportSync(); // from AdLdap2 from v5, disabling and falling back to v4's sync code
|
||||
$users = collect(Ldap::findLdapUsers())->slice(0, 11)->filter(function ($value, $key) { //choosing ELEVEN because one is going to be the count, which we're about to filter out in the next line
|
||||
return is_int($key);
|
||||
})->map(function ($item) use ($settings) {
|
||||
return (object) [
|
||||
'username' => $item[$settings['ldap_username_field']][0] ?? null,
|
||||
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
|
||||
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
|
||||
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
|
||||
'email' => $item[$settings['ldap_email']][0] ?? null,
|
||||
];
|
||||
});
|
||||
if ($users->count() > 0) {
|
||||
$message['user_sync'] = [
|
||||
'users' => $users,
|
||||
];
|
||||
} else {
|
||||
$message['user_sync'] = [
|
||||
'message' => 'Connection to LDAP was successful, however there were no users returned from your query. You should confirm the Base Bind DN above.',
|
||||
$connection = Ldap::connectToLdap();
|
||||
try {
|
||||
$message['bind'] = ['message' => 'Successfully bound to LDAP server.'];
|
||||
\Log::debug('attempting to bind to LDAP for LDAP test');
|
||||
Ldap::bindAdminToLdap($connection);
|
||||
$message['login'] = [
|
||||
'message' => 'Successfully connected to LDAP server.',
|
||||
];
|
||||
|
||||
return response()->json($message, 400);
|
||||
$users = collect(Ldap::findLdapUsers(null,10))->filter(function ($value, $key) {
|
||||
return is_int($key);
|
||||
})->slice(0, 10)->map(function ($item) use ($settings) {
|
||||
return (object) [
|
||||
'username' => $item[$settings['ldap_username_field']][0] ?? null,
|
||||
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
|
||||
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
|
||||
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
|
||||
'email' => $item[$settings['ldap_email']][0] ?? null,
|
||||
];
|
||||
});
|
||||
if ($users->count() > 0) {
|
||||
$message['user_sync'] = [
|
||||
'users' => $users,
|
||||
];
|
||||
} else {
|
||||
$message['user_sync'] = [
|
||||
'message' => 'Connection to LDAP was successful, however there were no users returned from your query. You should confirm the Base Bind DN above.',
|
||||
];
|
||||
|
||||
return response()->json($message, 400);
|
||||
}
|
||||
|
||||
return response()->json($message, 200);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('Bind failed');
|
||||
\Log::debug("Exception was: ".$e->getMessage());
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
//return response()->json(['message' => $e->getMessage()], 500);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
Log::info('LDAP sync failed');
|
||||
$message['user_sync'] = [
|
||||
'message' => 'Error getting users from LDAP directory, error: '.$ex->getMessage(),
|
||||
];
|
||||
|
||||
return response()->json($message, 400);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('Connection failed but we cannot debug it any further on our end.');
|
||||
return response()->json(['message' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return response()->json($message, 200);
|
||||
|
||||
}
|
||||
|
||||
public function ldaptestlogin(Request $request, LdapAd $ldap)
|
||||
public function ldaptestlogin(Request $request)
|
||||
{
|
||||
|
||||
if (Setting::getSettings()->ldap_enabled != '1') {
|
||||
|
@ -129,40 +93,50 @@ class SettingsController extends Controller
|
|||
}
|
||||
|
||||
|
||||
$rules = [
|
||||
$rules = array(
|
||||
'ldaptest_user' => 'required',
|
||||
'ldaptest_password' => 'required',
|
||||
];
|
||||
'ldaptest_password' => 'required'
|
||||
);
|
||||
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
if ($validator->fails()) {
|
||||
\Log::debug('LDAP Validation test failed.');
|
||||
$validation_errors = implode(' ', $validator->errors()->all());
|
||||
|
||||
$validation_errors = implode(' ',$validator->errors()->all());
|
||||
return response()->json(['message' => $validator->errors()->all()], 400);
|
||||
}
|
||||
|
||||
|
||||
|
||||
\Log::debug('Preparing to test LDAP login');
|
||||
try {
|
||||
DB::beginTransaction(); //this was the easiest way to invoke a full test of an LDAP login without adding new users to the DB (which may not be desired)
|
||||
$connection = Ldap::connectToLdap();
|
||||
try {
|
||||
Ldap::bindAdminToLdap($connection);
|
||||
\Log::debug('Attempting to bind to LDAP for LDAP test');
|
||||
try {
|
||||
$ldap_user = Ldap::findAndBindUserLdap($request->input('ldaptest_user'), $request->input('ldaptest_password'));
|
||||
if ($ldap_user) {
|
||||
\Log::debug('It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.');
|
||||
return response()->json(['message' => 'It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.'], 200);
|
||||
}
|
||||
return response()->json(['message' => 'Login Failed. '. $request->input('ldaptest_user').' did not successfully bind to LDAP.'], 400);
|
||||
|
||||
// $results = $ldap->ldap->auth()->attempt($request->input('ldaptest_username'), $request->input('ldaptest_password'), true);
|
||||
// can't do this because that's a protected property.
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('LDAP login failed');
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
}
|
||||
|
||||
$results = $ldap->ldapLogin($request->input('ldaptest_user'), $request->input('ldaptest_password')); // this would normally create a user on success (if they didn't already exist), but for the transaction
|
||||
if ($results) {
|
||||
return response()->json(['message' => 'It worked! '.$request->input('ldaptest_user').' successfully binded to LDAP.'], 200);
|
||||
} else {
|
||||
return response()->json(['message' => 'Login Failed. '.$request->input('ldaptest_user').' did not successfully bind to LDAP.'], 400);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('Bind failed');
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
//return response()->json(['message' => $e->getMessage()], 500);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('Connection failed');
|
||||
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
} finally {
|
||||
DB::rollBack(); // ALWAYS rollback, whether success or failure
|
||||
return response()->json(['message' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function slacktest(SlackSettingsRequest $request)
|
||||
|
@ -215,10 +189,9 @@ class SettingsController extends Controller
|
|||
*/
|
||||
public function ajaxTestEmail()
|
||||
{
|
||||
if (! config('app.lock_passwords')) {
|
||||
if (!config('app.lock_passwords')) {
|
||||
try {
|
||||
Notification::send(Setting::first(), new MailTest());
|
||||
|
||||
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 500);
|
||||
|
@ -291,4 +264,4 @@ class SettingsController extends Controller
|
|||
|
||||
return (new LoginAttemptsTransformer)->transformLoginAttempts($login_attempt_results, $total);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ namespace App\Http\Controllers\Auth;
|
|||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\LdapAd;
|
||||
use App\Models\Ldap;
|
||||
use App\Services\Saml;
|
||||
use Com\Tecnick\Barcode\Barcode;
|
||||
use Google2FA;
|
||||
|
@ -39,11 +39,6 @@ class LoginController extends Controller
|
|||
*/
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* @var LdapAd
|
||||
*/
|
||||
protected $ldap;
|
||||
|
||||
/**
|
||||
* @var Saml
|
||||
*/
|
||||
|
@ -52,12 +47,11 @@ class LoginController extends Controller
|
|||
/**
|
||||
* Create a new authentication controller instance.
|
||||
*
|
||||
* @param LdapAd $ldap
|
||||
* @param Saml $saml
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(/*LdapAd $ldap, */ Saml $saml)
|
||||
public function __construct(Saml $saml)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware('guest', ['except' => ['logout', 'postTwoFactorAuth', 'getTwoFactorAuth', 'getTwoFactorEnroll']]);
|
||||
|
@ -141,13 +135,47 @@ class LoginController extends Controller
|
|||
*/
|
||||
private function loginViaLdap(Request $request): User
|
||||
{
|
||||
$ldap = \App::make(LdapAd::class);
|
||||
try {
|
||||
return $ldap->ldapLogin($request->input('username'), $request->input('password'));
|
||||
} catch (\Exception $ex) {
|
||||
LOG::debug('LDAP user login: '.$ex->getMessage());
|
||||
throw new \Exception($ex->getMessage());
|
||||
}
|
||||
Log::debug("Binding user to LDAP.");
|
||||
$ldap_user = Ldap::findAndBindUserLdap($request->input('username'), $request->input('password'));
|
||||
if (!$ldap_user) {
|
||||
Log::debug("LDAP user ".$request->input('username')." not found in LDAP or could not bind");
|
||||
throw new \Exception("Could not find user in LDAP directory");
|
||||
} else {
|
||||
Log::debug("LDAP user ".$request->input('username')." successfully bound to LDAP");
|
||||
}
|
||||
|
||||
// Check if the user already exists in the database and was imported via LDAP
|
||||
$user = User::where('username', '=', $request->input('username'))->whereNull('deleted_at')->where('ldap_import', '=', 1)->where('activated', '=', '1')->first(); // FIXME - if we get more than one we should fail. and we sure about this ldap_import thing?
|
||||
Log::debug("Local auth lookup complete");
|
||||
|
||||
// The user does not exist in the database. Try to get them from LDAP.
|
||||
// If user does not exist and authenticates successfully with LDAP we
|
||||
// will create it on the fly and sign in with default permissions
|
||||
if (!$user) {
|
||||
Log::debug("Local user ".$request->input('username')." does not exist");
|
||||
Log::debug("Creating local user ".$request->input('username'));
|
||||
|
||||
if ($user = Ldap::createUserFromLdap($ldap_user)) { //this handles passwords on its own
|
||||
Log::debug("Local user created.");
|
||||
} else {
|
||||
Log::debug("Could not create local user.");
|
||||
throw new \Exception("Could not create local user");
|
||||
}
|
||||
// If the user exists and they were imported from LDAP already
|
||||
} else {
|
||||
Log::debug("Local user ".$request->input('username')." exists in database. Updating existing user against LDAP.");
|
||||
|
||||
$ldap_attr = Ldap::parseAndMapLdapAttributes($ldap_user);
|
||||
|
||||
if (Setting::getSettings()->ldap_pw_sync=='1') {
|
||||
$user->password = bcrypt($request->input('password'));
|
||||
}
|
||||
$user->email = $ldap_attr['email'];
|
||||
$user->first_name = $ldap_attr['firstname'];
|
||||
$user->last_name = $ldap_attr['lastname']; //FIXME (or TODO?) - do we need to map additional fields that we now support? E.g. country, phone, etc.
|
||||
$user->save();
|
||||
} // End if(!user)
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function loginViaRemoteUser(Request $request)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -38,11 +38,14 @@ class LoginForm extends Component
|
|||
$this->can_submit = false;
|
||||
}
|
||||
|
||||
$this->validateOnly($fields);
|
||||
|
||||
$this->can_submit = true;
|
||||
|
||||
$whatever = $this->validateOnly($fields);
|
||||
//\Log::info(print_r($whatever,true));
|
||||
|
||||
$errors = $this->getErrorBag();
|
||||
|
||||
$this->can_submit = $this->username !== "" && $this->password !== "" && !$errors->has('username') && !$errors->has('password') ; // wait, what?
|
||||
|
||||
\Log::info("Oy - can we submit yet?!".$this->can_submit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,7 +61,7 @@ class LoginForm extends Component
|
|||
public function submitForm()
|
||||
{
|
||||
|
||||
$this->can_submit = true;
|
||||
//$this->can_submit = true;
|
||||
|
||||
if (auth()->attempt($this->validate())) {
|
||||
return redirect()->intended('/');
|
||||
|
|
|
@ -9,6 +9,22 @@ use Illuminate\Database\Eloquent\Model;
|
|||
use Input;
|
||||
use Log;
|
||||
|
||||
/***********************************************
|
||||
* TODOS:
|
||||
*
|
||||
* First off, we should probably make it so that the main LDAP thing we're using is an *instance* of this class,
|
||||
* rather than the static methods we use here. We should probably load up that class with its settings, so we
|
||||
* don't have to explicitly refer to them so often.
|
||||
*
|
||||
* Then, we should probably look at embedding some of the logic we use elsewhere into here - the various methods
|
||||
* should either return a User or false, or other things like that. Don't make the consumers of this class reach
|
||||
* into its guts. While that conflates this model with the User model, I think having the appropriate logic for
|
||||
* turning LDAP people into Users ought to belong here, so it's easier on the consumer of this class.
|
||||
*
|
||||
* We're probably going to have to eventually make it so that Snipe-IT users can define multiple LDAP servers,
|
||||
* and having this as a more instance-oriented class will be a step in the right direction.
|
||||
***********************************************/
|
||||
|
||||
class Ldap extends Model
|
||||
{
|
||||
/**
|
||||
|
@ -87,22 +103,40 @@ class Ldap extends Model
|
|||
if ($ldap_username_field == 'userprincipalname') {
|
||||
$userDn = $username;
|
||||
} else {
|
||||
// In case they haven't added an AD domain
|
||||
// TODO - we no longer respect the "add AD Domain to username" checkbox, but it still exists in settings.
|
||||
// We should probably just eliminate that checkbox to avoid confusion.
|
||||
// We let it sit in the DB, unused, to facilitate people downgrading (if they decide to).
|
||||
// Hopefully, in a later release, we can remove it from the settings.
|
||||
// This logic instead just means that if we're using UPN, we don't append ad_domain, if we aren't, then we do.
|
||||
// Hopefully that should handle all of our use cases, but if not we can backport our old logic.
|
||||
$userDn = ($settings->ad_domain != '') ? $username.'@'.$settings->ad_domain : $username.'@'.$settings->email_domain;
|
||||
}
|
||||
}
|
||||
|
||||
\Log::debug('Attempting to login using distinguished name:'.$userDn);
|
||||
|
||||
$filterQuery = $settings->ldap_auth_filter_query.$username;
|
||||
$filter = Setting::getSettings()->ldap_filter;
|
||||
$filter = Setting::getSettings()->ldap_filter; //FIXME - this *does* respect the ldap filter, but I believe that AdLdap2 did *not*.
|
||||
$filterQuery = "({$filter}({$filterQuery}))";
|
||||
|
||||
\Log::debug('Filter query: '.$filterQuery);
|
||||
|
||||
if (! $ldapbind = @ldap_bind($connection, $userDn, $password)) {
|
||||
\Log::debug("Status of binding user: $userDn to directory: (directly!) ".($ldapbind ? "success" : "FAILURE"));
|
||||
if (! $ldapbind = self::bindAdminToLdap($connection)) {
|
||||
return false;
|
||||
/*
|
||||
* TODO PLEASE:
|
||||
*
|
||||
* this isn't very clear, so it's important to note: the $ldapbind value is never correctly returned - we never 'return true' from self::bindAdminToLdap() (the function
|
||||
* just "falls off the end" without ever explictly returning 'true')
|
||||
*
|
||||
* but it *does* have an interesting side-effect of checking for the LDAP password being incorrectly encrypted with the wrong APP_KEY, so I'm leaving it in for now.
|
||||
*
|
||||
* If it *did* correctly return 'true' on a succesful bind, it would _probably_ allow users to log in with an incorrect password. Which would be horrible!
|
||||
*
|
||||
* Let's definitely fix this at the next refactor!!!!
|
||||
*
|
||||
*/
|
||||
\Log::debug("Status of binding Admin user: $userDn to directory instead: ".($ldapbind ? "success" : "FAILURE"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,8 +169,6 @@ class Ldap extends Model
|
|||
{
|
||||
$ldap_username = Setting::getSettings()->ldap_uname;
|
||||
|
||||
$ldap_username = Setting::getSettings()->ldap_uname;
|
||||
|
||||
// Lets return some nicer messages for users who donked their app key, and disable LDAP
|
||||
try {
|
||||
$ldap_pass = \Crypt::decrypt(Setting::getSettings()->ldap_pword);
|
||||
|
@ -147,6 +179,11 @@ class Ldap extends Model
|
|||
if (! $ldapbind = @ldap_bind($connection, $ldap_username, $ldap_pass)) {
|
||||
throw new Exception('Could not bind to LDAP: '.ldap_error($connection));
|
||||
}
|
||||
// TODO - this just "falls off the end" but the function states that it should return true or false
|
||||
// unfortunately, one of the use cases for this function is wrong and *needs* for that failure mode to fire
|
||||
// so I don't want to fix this right now.
|
||||
// this method MODIFIES STATE on the passed-in $connection and just returns true or false (or, in this case, undefined)
|
||||
// at the next refactor, this should be appropriately modified to be more consistent.
|
||||
}
|
||||
|
||||
|
||||
|
@ -233,14 +270,14 @@ class Ldap extends Model
|
|||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v3.0]
|
||||
* @param $ldapatttibutes
|
||||
* @param $base_dn
|
||||
* @param $count
|
||||
* @return array|bool
|
||||
*/
|
||||
public static function findLdapUsers($base_dn = null)
|
||||
public static function findLdapUsers($base_dn = null, $count = -1)
|
||||
{
|
||||
$ldapconn = self::connectToLdap();
|
||||
$ldap_bind = self::bindAdminToLdap($ldapconn);
|
||||
self::bindAdminToLdap($ldapconn);
|
||||
// Default to global base DN if nothing else is provided.
|
||||
if (is_null($base_dn)) {
|
||||
$base_dn = Setting::getSettings()->ldap_basedn;
|
||||
|
@ -256,40 +293,58 @@ class Ldap extends Model
|
|||
// Perform the search
|
||||
do {
|
||||
|
||||
// Paginate (non-critical, if not supported by server)
|
||||
if (! $ldap_paging = @ldap_control_paged_result($ldapconn, $page_size, false, $cookie)) {
|
||||
throw new Exception('Problem with your LDAP connection. Try checking the Use TLS setting in Admin > Settings. ');
|
||||
}
|
||||
|
||||
if ($filter != '' && substr($filter, 0, 1) != '(') { // wrap parens around NON-EMPTY filters that DON'T have them, for back-compatibility with AdLdap2-based filters
|
||||
$filter = "($filter)";
|
||||
} elseif ($filter == '') {
|
||||
$filter = '(cn=*)';
|
||||
}
|
||||
|
||||
$search_results = ldap_search($ldapconn, $base_dn, $filter);
|
||||
|
||||
// HUGE thanks to this article: https://stackoverflow.com/questions/68275972/how-to-get-paged-ldap-queries-in-php-8-and-read-more-than-1000-entries
|
||||
// which helped me wrap my head around paged results!
|
||||
\Log::info("ldap conn is: ".$ldapconn." basedn is: $base_dn, filter is: $filter - count is: $count. page size is: $page_size"); //FIXME - remove
|
||||
// if a $count is set and it's smaller than $page_size then use that as the page size
|
||||
$ldap_controls = [];
|
||||
//if($count == -1) { //count is -1 means we have to employ paging to query the entire directory
|
||||
$ldap_controls = [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'iscritical' => false, 'value' => ['size'=> $count == -1||$count>$page_size ? $page_size : $count, 'cookie' => $cookie]]];
|
||||
//}
|
||||
$search_results = ldap_search($ldapconn, $base_dn, $filter, [], 0, /* $page_size */ -1, -1, LDAP_DEREF_NEVER, $ldap_controls); // TODO - I hate the @, and I hate that we get a full page even if we ask for 10 records. Can we use an ldap_control?
|
||||
\Log::info("did the search run? I guess so if you got here!");
|
||||
if (! $search_results) {
|
||||
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_search').ldap_error($ldapconn)); // FIXME this is never called in any routed context - only from the Artisan command. So this redirect will never work.
|
||||
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_search').ldap_error($ldapconn)); // TODO this is never called in any routed context - only from the Artisan command. So this redirect will never work.
|
||||
}
|
||||
|
||||
$errcode = null;
|
||||
$matcheddn = null;
|
||||
$errmsg = null;
|
||||
$referrals = null;
|
||||
$controls = [];
|
||||
ldap_parse_result($ldapconn, $search_results, $errcode , $matcheddn , $errmsg , $referrals, $controls);
|
||||
if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
|
||||
// You need to pass the cookie from the last call to the next one
|
||||
$cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
|
||||
\Log::debug("okay, at least one more page to go!!!");
|
||||
} else {
|
||||
\Log::debug("okay, we're out of pages - no cookie (or empty cookie) was passed");
|
||||
$cookie = '';
|
||||
}
|
||||
// Empty cookie means last page
|
||||
|
||||
// Get results from page
|
||||
$results = ldap_get_entries($ldapconn, $search_results);
|
||||
if (! $results) {
|
||||
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_get_entries').ldap_error($ldapconn)); // FIXME this is never called in any routed context - only from the Artisan command. So this redirect will never work.
|
||||
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_get_entries').ldap_error($ldapconn)); // TODO this is never called in any routed context - only from the Artisan command. So this redirect will never work.
|
||||
}
|
||||
|
||||
// Add results to result set
|
||||
$global_count += $results['count'];
|
||||
$result_set = array_merge($result_set, $results);
|
||||
\Log::debug("Total count is: $global_count");
|
||||
|
||||
@ldap_control_paged_result_response($ldapconn, $search_results, $cookie);
|
||||
} while ($cookie !== null && $cookie != '');
|
||||
} while ($cookie !== null && $cookie != '' && ($count == -1 || $global_count < $count)); // some servers don't even have pagination, and some will give you more results than you asked for, so just see if you have enough.
|
||||
|
||||
// Clean up after search
|
||||
$result_set['count'] = $global_count;
|
||||
$result_set['count'] = $global_count; // TODO: I would've figured you could just count the array instead?
|
||||
$results = $result_set;
|
||||
@ldap_control_paged_result($ldapconn, 0);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\LdapAd;
|
||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class LdapServiceProvider extends ServiceProvider implements DeferrableProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(LdapAd::class, LdapAd::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return [LdapAd::class];
|
||||
}
|
||||
}
|
|
@ -1,572 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Adldap\Adldap;
|
||||
use Adldap\Models\User as AdldapUser;
|
||||
use Adldap\Query\Paginator;
|
||||
use Adldap\Schemas\Schema;
|
||||
use App\Helpers\Helper;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* LDAP queries.
|
||||
*
|
||||
* @author Wes Hulette <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
* @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 <jwhulette@gmail.com>
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
|
@ -1,311 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* LDAP configuration merge for Adldap2.
|
||||
*
|
||||
* @see https://github.com/Adldap2/Adldap2
|
||||
*
|
||||
* @author Wes Hulette <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
private function setSnipeItConfig()
|
||||
{
|
||||
$this->ldapConfig = $this->setLdapConnectionConfiguration();
|
||||
$this->certificateCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LDAP settings from the Settings model.
|
||||
*
|
||||
* @author Wes Hulette <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <jwhulette@gmail.com>
|
||||
*
|
||||
* @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 <sb@sbuehl.com>
|
||||
*
|
||||
* @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 <sb@sbuehl.com>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@
|
|||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"adldap2/adldap2": "^10.2",
|
||||
"alek13/slack": "^2.0",
|
||||
"bacon/bacon-qr-code": "^2.0",
|
||||
"barryvdh/laravel-debugbar": "^3.6",
|
||||
|
|
661
composer.lock
generated
661
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -345,7 +345,6 @@ return [
|
|||
* Custom service provider
|
||||
*/
|
||||
App\Providers\MacroServiceProvider::class,
|
||||
App\Providers\LdapServiceProvider::class,
|
||||
App\Providers\SamlServiceProvider::class,
|
||||
|
||||
],
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<i class="fas fa-user" aria-hidden="true"></i>
|
||||
{{ trans('admin/users/table.username') }}
|
||||
</label>
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.username') }}" name="username" type="text" autocomplete="off" wire:model="username" wire:model.defer="username" wire:model.lazy="username">
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.username') }}" name="username" type="text" autocomplete="off" wire:model="username">
|
||||
@error('username')
|
||||
<span class="alert-msg">
|
||||
{{ $message }}
|
||||
|
@ -53,7 +53,7 @@
|
|||
<i class="fas fa-key" aria-hidden="true"></i>
|
||||
{{ trans('admin/users/table.password') }}
|
||||
</label>
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.password') }}" name="password" type="password" wire:model="password" wire:model.lazy="password" wire:model.defer="password" autocomplete="off">
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.password') }}" name="password" type="password" wire:model="password" autocomplete="off">
|
||||
@error('password')
|
||||
<span class="alert-msg">
|
||||
{{ $message }}
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
</div>
|
||||
</div><!-- AD Domain -->
|
||||
|
||||
<!-- AD Append Domain -->
|
||||
{{-- NOTICE - this was a feature for AdLdap2-based LDAP syncing, and is already handled in 'classic' LDAP, so we now hide the checkbox (but haven't deleted the field) <!-- AD Append Domain -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-3">
|
||||
{{ Form::label('ad_append_domain', trans('admin/settings/general.ad_append_domain_label')) }}
|
||||
|
@ -136,7 +136,7 @@
|
|||
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div> --}}
|
||||
|
||||
<!-- LDAP Client-Side TLS key -->
|
||||
<div class="form-group {{ $errors->has('ldap_client_tls_key') ? 'error' : '' }}">
|
||||
|
|
|
@ -702,10 +702,10 @@ Route::group(['prefix' => 'v1', 'middleware' => 'api'], function () {
|
|||
*/
|
||||
Route::group(['middleware'=> ['auth', 'authorize:superuser'], 'prefix' => 'settings'], function () {
|
||||
|
||||
Route::post('ldaptest',
|
||||
Route::get('ldaptest',
|
||||
[
|
||||
Api\SettingsController::class,
|
||||
'ldapAdSettingsTest'
|
||||
'ldaptest'
|
||||
]
|
||||
)->name('api.settings.ldaptest');
|
||||
|
||||
|
|
Loading…
Reference in a new issue