mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 03:06:23 +00:00
Merge pull request #19439 from grokability/ldap-sync-mapping
LDAP: fixed typos, deliver the summary in a table in cli mode
This commit is contained in:
@ -3,7 +3,6 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Department;
|
||||
use App\Models\Group;
|
||||
use App\Models\Ldap;
|
||||
use App\Models\Location;
|
||||
@ -55,27 +54,12 @@ class LdapSync extends Command
|
||||
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); // 600 seconds = 10 minutes
|
||||
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
|
||||
|
||||
// Map the LDAP attributes to the Snipe-IT user fields.
|
||||
$ldap_map = [
|
||||
'username' => Setting::getSettings()->ldap_username_field,
|
||||
'last_name' => Setting::getSettings()->ldap_lname_field,
|
||||
'first_name' => Setting::getSettings()->ldap_fname_field,
|
||||
'active_flag' => Setting::getSettings()->ldap_active_flag,
|
||||
'emp_num' => Setting::getSettings()->ldap_emp_num,
|
||||
'email' => Setting::getSettings()->ldap_email,
|
||||
'phone' => Setting::getSettings()->ldap_phone_field,
|
||||
'mobile' => Setting::getSettings()->ldap_mobile,
|
||||
'jobtitle' => Setting::getSettings()->ldap_jobtitle,
|
||||
'address' => Setting::getSettings()->ldap_address,
|
||||
'city' => Setting::getSettings()->ldap_city,
|
||||
'state' => Setting::getSettings()->ldap_state,
|
||||
'zip' => Setting::getSettings()->ldap_zip,
|
||||
'country' => Setting::getSettings()->ldap_country,
|
||||
'location' => Setting::getSettings()->ldap_location,
|
||||
'dept' => Setting::getSettings()->ldap_dept,
|
||||
'manager' => Setting::getSettings()->ldap_manager,
|
||||
'display_name' => Setting::getSettings()->ldap_display_name,
|
||||
];
|
||||
// Single source of truth for internal-key => LDAP-attribute-name
|
||||
// lives on the Ldap model so parseAndMapLdapAttributes and this
|
||||
// command can't drift. Used here for the LDAP query attribute
|
||||
// list plus a handful of specific-lookup gates (activated,
|
||||
// manager, location, username) that only LdapSync needs.
|
||||
$ldap_map = Ldap::attributeMap();
|
||||
|
||||
$ldap_default_group = Setting::getSettings()->ldap_default_group;
|
||||
$search_base = Setting::getSettings()->ldap_base_dn;
|
||||
@ -133,7 +117,7 @@ class LdapSync extends Command
|
||||
*/
|
||||
$attributes = array_values(array_filter($ldap_map));
|
||||
|
||||
if (Setting::getSettings()->is_ad === 1 && is_null($ldap_map['active_flag'])) {
|
||||
if (Setting::getSettings()->is_ad === 1 && is_null($ldap_map['activated'])) {
|
||||
$attributes[] = 'useraccountcontrol';
|
||||
}
|
||||
|
||||
@ -243,37 +227,15 @@ class LdapSync extends Command
|
||||
|
||||
// Assign the mapped LDAP attributes for each user to the Snipe-IT user fields
|
||||
for ($i = 0; $i < $results['count']; $i++) {
|
||||
$item = [];
|
||||
$item['username'] = $results[$i][$ldap_map['username']][0] ?? null;
|
||||
$item['display_name'] = $results[$i][$ldap_map['display_name']][0] ?? null;
|
||||
$item['employee_number'] = $results[$i][$ldap_map['emp_num']][0] ?? null;
|
||||
$item['lastname'] = $results[$i][$ldap_map['last_name']][0] ?? null;
|
||||
$item['firstname'] = $results[$i][$ldap_map['first_name']][0] ?? null;
|
||||
$item['email'] = $results[$i][$ldap_map['email']][0] ?? null;
|
||||
// parseAndMapLdapAttributes is the shared parser used by the
|
||||
// first-login create path too, so the two flows can't drift
|
||||
// on field names / lookup shape. The two OU-override keys
|
||||
// (ldap_location_override, location_id) are LdapSync-only,
|
||||
// injected earlier by the OU sweep at line 191 or so, so we
|
||||
// stitch them back on here.
|
||||
$item = Ldap::parseAndMapLdapAttributes($results[$i]);
|
||||
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? null;
|
||||
$item['location_id'] = $results[$i]['location_id'] ?? null;
|
||||
$item['telephone'] = $results[$i][$ldap_map['phone']][0] ?? null;
|
||||
$item['mobile'] = $results[$i][$ldap_map['mobile']][0] ?? null;
|
||||
$item['jobtitle'] = $results[$i][$ldap_map['jobtitle']][0] ?? null;
|
||||
$item['address'] = $results[$i][$ldap_map['address']][0] ?? null;
|
||||
$item['city'] = $results[$i][$ldap_map['city']][0] ?? null;
|
||||
$item['state'] = $results[$i][$ldap_map['state']][0] ?? null;
|
||||
$item['country'] = $results[$i][$ldap_map['country']][0] ?? null;
|
||||
$item['zip'] = $results[$i][$ldap_map['zip']][0] ?? null;
|
||||
$item['department'] = $results[$i][$ldap_map['dept']][0] ?? null;
|
||||
$item['manager'] = $results[$i][$ldap_map['manager']][0] ?? null;
|
||||
$item['location'] = $results[$i][$ldap_map['location']][0] ?? null;
|
||||
$location = $default_location; // initially, set '$location' to the default_location (which may just be null)
|
||||
|
||||
// ONLY if you are using the "ldap_location" option *AND* you have an actual result
|
||||
if ($ldap_map['location'] && $item['location']) {
|
||||
$location = Location::firstOrCreate([
|
||||
'name' => $item['location'],
|
||||
]);
|
||||
}
|
||||
$department = Department::firstOrCreate([
|
||||
'name' => $item['department'],
|
||||
]);
|
||||
|
||||
$user = User::withTrashed()->where('username', $item['username'])->first();
|
||||
if (! empty($item['username'])) {
|
||||
@ -294,55 +256,14 @@ class LdapSync extends Command
|
||||
$item['createorupdate'] = 'created';
|
||||
}
|
||||
|
||||
// If a sync option is not filled in on the LDAP settings don't populate the user field
|
||||
if ($ldap_map['username'] != null) {
|
||||
$user->username = $item['username'];
|
||||
}
|
||||
if ($ldap_map['display_name'] != null) {
|
||||
$user->display_name = $item['display_name'];
|
||||
}
|
||||
if ($ldap_map['last_name'] != null) {
|
||||
$user->last_name = $item['lastname'];
|
||||
}
|
||||
if ($ldap_map['first_name'] != null) {
|
||||
$user->first_name = $item['firstname'];
|
||||
}
|
||||
if ($ldap_map['emp_num'] != null) {
|
||||
$user->employee_num = e($item['employee_number']);
|
||||
}
|
||||
if ($ldap_map['email'] != null) {
|
||||
$user->email = $item['email'];
|
||||
}
|
||||
if ($ldap_map['phone'] != null) {
|
||||
$user->phone = $item['telephone'];
|
||||
}
|
||||
if ($ldap_map['mobile'] != null) {
|
||||
$user->mobile = $item['mobile'];
|
||||
}
|
||||
if ($ldap_map['jobtitle'] != null) {
|
||||
$user->jobtitle = $item['jobtitle'];
|
||||
}
|
||||
if ($ldap_map['address'] != null) {
|
||||
$user->address = $item['address'];
|
||||
}
|
||||
if ($ldap_map['city'] != null) {
|
||||
$user->city = $item['city'];
|
||||
}
|
||||
if ($ldap_map['state'] != null) {
|
||||
$user->state = $item['state'];
|
||||
}
|
||||
if ($ldap_map['country'] != null) {
|
||||
$user->country = $item['country'];
|
||||
}
|
||||
if ($ldap_map['zip'] != null) {
|
||||
$user->zip = $item['zip'];
|
||||
}
|
||||
if ($ldap_map['dept'] != null) {
|
||||
$user->department_id = $department->id;
|
||||
}
|
||||
if ($ldap_map['location'] != null) {
|
||||
$user->location_id = $location?->id;
|
||||
}
|
||||
// Shared field-write path with the first-login create flow.
|
||||
// Handles every mapped scalar field, plus Department and
|
||||
// Location firstOrCreate for the LDAP-derived values. The
|
||||
// three LdapSync-only concerns (manager LDAP re-query,
|
||||
// activated / UAC, OU location override) are handled
|
||||
// inline below because they don't apply to the first-login
|
||||
// path.
|
||||
Ldap::applyLdapAttributesToUser($user, $item);
|
||||
|
||||
if ($ldap_map['manager'] != null) {
|
||||
if ($item['manager'] != null) {
|
||||
@ -394,10 +315,10 @@ class LdapSync extends Command
|
||||
}
|
||||
|
||||
// Sync activated state for Active Directory.
|
||||
if (! empty($ldap_map['active_flag'])) { // IF we have an 'active' flag set....
|
||||
if (! empty($ldap_map['activated'])) { // IF we have an 'active' flag set....
|
||||
// ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
|
||||
// (Specifically, we don't handle a value of '0.0' correctly)
|
||||
$raw_value = @$results[$i][$ldap_map['active_flag']][0];
|
||||
$raw_value = @$results[$i][$ldap_map['activated']][0];
|
||||
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
|
||||
$boolean_cast = (bool) $raw_value;
|
||||
@ -449,19 +370,28 @@ class LdapSync extends Command
|
||||
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
|
||||
already-existing accounts will be however the administrator has set them */
|
||||
|
||||
// Location resolution: applyLdapAttributesToUser above has
|
||||
// already written location_id from the LDAP payload when a
|
||||
// value was present. This block layers on the two overrides
|
||||
// it doesn't know about: the OU-based override wins over
|
||||
// everything when set, and the --location CLI flag fills in
|
||||
// when neither the OU override nor an LDAP-derived location
|
||||
// applied to this run.
|
||||
$ldapProvidedLocation = $ldap_map['location'] !== null && $item['location'] !== '';
|
||||
|
||||
if ($item['ldap_location_override'] == true) {
|
||||
$user->location_id = $item['location_id'];
|
||||
} elseif ((isset($location)) && (! empty($location))) {
|
||||
if ((is_array($location)) && (array_key_exists('id', $location))) {
|
||||
$user->location_id = $location['id'];
|
||||
} elseif (is_object($location)) {
|
||||
$user->location_id = $location->id; // THIS is the magic line, this should do it.
|
||||
}
|
||||
} elseif (! $ldapProvidedLocation && ! empty($default_location)) {
|
||||
$user->location_id = is_array($default_location)
|
||||
? $default_location['id']
|
||||
: $default_location->id;
|
||||
}
|
||||
// TODO - should we be NULLING locations if $location is really `null`, and that's what we came up with?
|
||||
// will that conflict with any overriding setting that the user set? Like, if they moved someone from
|
||||
// the 'null' location to somewhere, we wouldn't want to try to override that, right?
|
||||
$location = null;
|
||||
// TODO - should we be NULLING locations when neither the OU
|
||||
// override, the LDAP payload, nor --location produced a
|
||||
// location for this user? Currently we leave whatever they
|
||||
// had, matching the pre-refactor behavior. Changing that
|
||||
// could clobber a location an admin set by hand.
|
||||
|
||||
$user->ldap_import = 1;
|
||||
|
||||
$errors = '';
|
||||
@ -505,13 +435,18 @@ class LdapSync extends Command
|
||||
$missing_ldap_users = $missing_ldap_users->get();
|
||||
|
||||
foreach ($missing_ldap_users as $missing_user) {
|
||||
$is_deletable = $this->isUserDeletable($missing_user);
|
||||
// Match the rule a manual "delete user" click uses. We
|
||||
// can't call User::isDeletable() directly here because
|
||||
// it wraps a Gate::allows('delete', $user) check that
|
||||
// needs an authenticated web-session user, and this
|
||||
// command runs from cron with no such user
|
||||
$is_deletable = $missing_user->hasNoAssignmentBlockers();
|
||||
|
||||
$missing_item = [
|
||||
'id' => $missing_user->id,
|
||||
'username' => $missing_user->username,
|
||||
'firstname' => $missing_user->first_name,
|
||||
'lastname' => $missing_user->last_name,
|
||||
'first_name' => $missing_user->first_name,
|
||||
'last_name' => $missing_user->last_name,
|
||||
'email' => $missing_user->email,
|
||||
'createorupdate' => 'skipped',
|
||||
'status' => 'info',
|
||||
@ -530,16 +465,19 @@ class LdapSync extends Command
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($this->option('summary')) {
|
||||
for ($x = 0; $x < count($summary); $x++) {
|
||||
if ($summary[$x]['status'] == 'error') {
|
||||
$this->error('ERROR: '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was not imported: '.$summary[$x]['note']);
|
||||
} else {
|
||||
$this->info('User '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was '.strtoupper($summary[$x]['createorupdate']).'.');
|
||||
}
|
||||
}
|
||||
$rows = array_map(fn ($row) => [
|
||||
$row['username'] ?? '',
|
||||
trim(($row['first_name'] ?? '').' '.($row['last_name'] ?? '')),
|
||||
strtoupper($row['createorupdate'] ?? ''),
|
||||
strtoupper($row['status'] ?? ''),
|
||||
$row['note'] ?? '',
|
||||
], $summary);
|
||||
|
||||
$this->table(
|
||||
['Username', 'Name', 'Action', 'Status', 'Note'],
|
||||
$rows,
|
||||
);
|
||||
} elseif ($this->option('json_summary')) {
|
||||
$json_summary = ['error' => false, 'error_message' => '', 'summary' => $summary]; // hardcoding the error to false and the error_message to blank seems a bit weird
|
||||
$this->info(json_encode($json_summary));
|
||||
@ -547,23 +485,4 @@ class LdapSync extends Command
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is deletable without gate check
|
||||
*
|
||||
* A user is considered deletable if they have no associated assets, accessories, licenses, consumables, managed users, or managed locations.
|
||||
*
|
||||
* @param User $user The user to check
|
||||
*
|
||||
* @return bool True if the user is deletable, false otherwise
|
||||
*/
|
||||
private function isUserDeletable(User $user): bool
|
||||
{
|
||||
return (($user->assets_count ?? $user->assets()->count()) === 0)
|
||||
&& (($user->accessories_count ?? $user->accessories()->count()) === 0)
|
||||
&& (($user->licenses_count ?? $user->licenses()->count()) === 0)
|
||||
&& (($user->consumables_count ?? $user->consumables()->count()) === 0)
|
||||
&& (($user->manages_users_count ?? $user->managesUsers()->count()) === 0)
|
||||
&& (($user->manages_locations_count ?? $user->managedLocations()->count()) === 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,12 @@ class ResetDemoSettings extends Command
|
||||
$settings->label2_2d_type = 'QRCODE';
|
||||
$settings->default_currency = 'USD';
|
||||
$settings->brand = 2;
|
||||
$settings->ldap_enabled = 0;
|
||||
// Enabled so the wizard's return-visitor branch unlocks all 5
|
||||
// steps for demo visitors and they can jump straight to the
|
||||
// step-3 Test Find User preview against the seeded Forumsys
|
||||
// config. Safe on the demo because LoginController skips the
|
||||
// LDAP auth branch when config('app.lock_passwords') is on.
|
||||
$settings->ldap_enabled = '1';
|
||||
$settings->full_multiple_companies_support = 0;
|
||||
$settings->label2_1d_type = 'C128';
|
||||
$settings->email_domain = 'snipeitapp.com';
|
||||
|
||||
@ -43,31 +43,30 @@ class SettingsController extends Controller
|
||||
'message' => 'Successfully connected to LDAP server.',
|
||||
];
|
||||
|
||||
// Shape is driven by Ldap::parseAndMapLdapAttributes so
|
||||
// this endpoint stays in sync with the sync command and
|
||||
// the first-login create path. Blank fields collapse to
|
||||
// null in the JSON so the JS side's `?? NULL` fallback
|
||||
// renders "NULL" for missing values.
|
||||
$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,
|
||||
'display_name' => $item[$settings['ldap_display_name']][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,
|
||||
'phone' => $item[$settings['ldap_phone_field']][0] ?? null,
|
||||
'mobile' => $item[$settings['ldap_mobile']][0] ?? null,
|
||||
'jobtitle' => $item[$settings['ldap_jobtitle']][0] ?? null,
|
||||
'department' => $item[$settings['ldap_department']][0] ?? null,
|
||||
'manager' => $item[$settings['ldap_manager']][0] ?? null,
|
||||
'address' => $item[$settings['ldap_address']][0] ?? null,
|
||||
'city' => $item[$settings['ldap_city']][0] ?? null,
|
||||
'state' => $item[$settings['ldap_state']][0] ?? null,
|
||||
'zip' => $item[$settings['ldap_zip']][0] ?? null,
|
||||
'country' => $item[$settings['ldap_country']][0] ?? null,
|
||||
'location' => $item[$settings['ldap_location']][0] ?? null,
|
||||
];
|
||||
})->slice(0, 10)->map(function ($item) {
|
||||
$mapped = Ldap::parseAndMapLdapAttributes($item);
|
||||
|
||||
return (object) array_map(fn ($value) => $value === '' ? null : $value, $mapped);
|
||||
});
|
||||
if ($users->count() > 0) {
|
||||
// `fields` is the ordered internal_key => translated
|
||||
// label map the JS iterates to build the results
|
||||
// table's header + per-row cells. Same shape drives
|
||||
// the LDAP wizard's step-3 preview, so both stay
|
||||
// consistent as attributeMap() grows.
|
||||
$labels = Ldap::attributeLabels();
|
||||
$fields = collect(array_keys(Ldap::parseAndMapLdapAttributes([])))
|
||||
->mapWithKeys(fn ($key) => [$key => $labels[$key] ?? $key])
|
||||
->all();
|
||||
$message['user_sync'] = [
|
||||
'fields' => $fields,
|
||||
'users' => $users,
|
||||
];
|
||||
} else {
|
||||
|
||||
@ -1043,7 +1043,13 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function syncLdapUsers(Request $request)
|
||||
{
|
||||
$this->authorize('update', User::class);
|
||||
// Superuser-only: a bulk LDAP sync surfaces users from across
|
||||
// the entire directory (all companies, all OUs), so anyone with
|
||||
// "users.edit" but no full-directory access shouldn't be able
|
||||
// to run it or read the summary that lists them.
|
||||
if (! auth()->user()?->isSuperUser()) {
|
||||
abort(403);
|
||||
}
|
||||
// Call Artisan LDAP import command.
|
||||
|
||||
Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);
|
||||
|
||||
@ -300,7 +300,7 @@ class LoginController extends Controller
|
||||
return redirect()->back()->withInput()->withErrors($validator);
|
||||
}
|
||||
|
||||
// Set the custom lockout attempts from the env and sett the custom lockout throttle from the env.
|
||||
// Set the custom lockout attempts from the env and set the custom lockout throttle from the env.
|
||||
// We divide decayMinutes by 60 here to get minutes, since Laravel changed the default from minutes
|
||||
// to seconds, and we don't want to break limits on existing systems
|
||||
$this->maxAttempts = config('auth.passwords.users.throttle.max_attempts');
|
||||
@ -314,8 +314,12 @@ class LoginController extends Controller
|
||||
|
||||
$user = null;
|
||||
|
||||
// Should we even check for LDAP users?
|
||||
if (Setting::getSettings()->ldap_enabled) { // avoid hitting the $this->ldap
|
||||
// Should we even check for LDAP users? Skip LDAP entirely when
|
||||
// the app is in demo mode. The LDAP wizard's
|
||||
// demo seed points at Forumsys as a reference config for
|
||||
// visitors to click through, we don't want the login form to
|
||||
// actually try to bind against it on every demo sign-in.
|
||||
if (Setting::getSettings()->ldap_enabled && !config('app.lock_passwords')) { // avoid hitting the $this->ldap
|
||||
Log::debug('LDAP is enabled.');
|
||||
try {
|
||||
Log::debug('Attempting to log user in by LDAP authentication.');
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Users;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@ -28,8 +27,13 @@ class LDAPImportController extends Controller
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// I guess this prolly oughtta... I dunno. Do something?
|
||||
$this->authorize('update', User::class);
|
||||
// Superuser-only: a bulk LDAP sync surfaces users from across
|
||||
// the entire directory (all companies, all OUs), so anyone with
|
||||
// "users.edit" but no full-directory access shouldn't be able
|
||||
// to run it or read the summary that lists them.
|
||||
if (! auth()->user()?->isSuperUser()) {
|
||||
abort(403);
|
||||
}
|
||||
try {
|
||||
// $this->ldap->connect(); I don't think this actually exists in LdapAd.php, and we don't really 'persist' LDAP connections anyways...right?
|
||||
} catch (\Exception $e) {
|
||||
@ -52,7 +56,10 @@ class LDAPImportController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorize('update', User::class);
|
||||
// See create() for the superuser-only rationale.
|
||||
if (! auth()->user()?->isSuperUser()) {
|
||||
abort(403);
|
||||
}
|
||||
// Call Artisan LDAP import command.
|
||||
|
||||
Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ldap;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
@ -77,6 +78,24 @@ class LdapSettings extends Component
|
||||
// finishWizard so back-nav doesn't retrigger the animation.
|
||||
public ?int $justCompletedStep = null;
|
||||
|
||||
// Read-only lock. Set from config('app.lock_passwords') in mount()
|
||||
// and blade uses it to render every wire:model input with the
|
||||
// `readonly` / `disabled` attribute so demo visitors can't retype
|
||||
// real LDAP creds into the wizard. Server-side enforcement lives
|
||||
// in updated(), which reverts any prop mutation back to the
|
||||
// persisted Setting values (defense against a caller that fakes
|
||||
// wire:model updates around the disabled UI).
|
||||
public bool $isReadOnly = false;
|
||||
|
||||
// Properties that stay editable even when isReadOnly is on. The
|
||||
// sample-username field on step 3 has to remain writable so the
|
||||
// Test Find User preview still works, which is the one wizard
|
||||
// interaction we do want demo visitors to exercise.
|
||||
private const READ_ONLY_ALLOWED_PROPS = [
|
||||
'currentStep',
|
||||
'test_sample_username',
|
||||
];
|
||||
|
||||
// Step 1: Connection
|
||||
public bool $ldap_enabled = false;
|
||||
|
||||
@ -184,6 +203,7 @@ class LdapSettings extends Component
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->isReadOnly = (bool) config('app.lock_passwords');
|
||||
$this->hydrateFromPersisted();
|
||||
|
||||
// Restore in-flight wizard progress from the session so a page
|
||||
@ -208,6 +228,17 @@ class LdapSettings extends Component
|
||||
}
|
||||
}
|
||||
|
||||
// Demo mode unlocks the wizard independent of ldap_enabled.
|
||||
// The save/advance methods are gated shut by lock_passwords
|
||||
// so a visitor with ldap_enabled=false would otherwise be
|
||||
// trapped on step 1 with no way to reach the Test Find User
|
||||
// preview on step 3. Unlocking the stepper here lets them
|
||||
// jump to any step. Fields stay locked via isReadOnly /
|
||||
// updated() enforcement.
|
||||
if ($this->isReadOnly) {
|
||||
$this->highestStepReached = 5;
|
||||
}
|
||||
|
||||
// Clamp against total step count in case a session pointer
|
||||
// was seeded when the wizard had a different step layout.
|
||||
$this->highestStepReached = min($this->highestStepReached, 5);
|
||||
@ -312,7 +343,18 @@ class LdapSettings extends Component
|
||||
|
||||
public function saveAndAdvance()
|
||||
{
|
||||
// Demo mode: nothing to save (isReadOnly + updated() lock the
|
||||
// fields to seeded values), but visitors still want to walk
|
||||
// the wizard forward step by step to see each screen. Skip
|
||||
// validation / network test / persist and just advance. The
|
||||
// per-step Test Bind / Test Find User buttons on individual
|
||||
// steps remain available for anyone who wants to fire a live
|
||||
// request against the seeded Forumsys config.
|
||||
if (config('app.lock_passwords')) {
|
||||
if ($this->currentStep < 5) {
|
||||
$this->goToStep($this->currentStep + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -770,38 +812,6 @@ class LdapSettings extends Component
|
||||
|
||||
// === Step 3: Attribute mapping ========================================= =============================
|
||||
|
||||
/**
|
||||
* Snipe-IT field name → LDAP-attribute-name Livewire property. Used
|
||||
* both by the preview-table render and by any future sync-side code
|
||||
* that wants a canonical map of "what field goes where." Order here
|
||||
* defines the order in the preview table.
|
||||
*
|
||||
* Any new mappings we create would need to go here too.
|
||||
*/
|
||||
protected function attributeMap(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->ldap_username_field,
|
||||
'first_name' => $this->ldap_fname_field,
|
||||
'last_name' => $this->ldap_lname_field,
|
||||
'display_name' => $this->ldap_display_name,
|
||||
'email' => $this->ldap_email,
|
||||
'employee_num' => $this->ldap_emp_num,
|
||||
'phone' => $this->ldap_phone_field,
|
||||
'mobile' => $this->ldap_mobile,
|
||||
'job_title' => $this->ldap_jobtitle,
|
||||
'manager' => $this->ldap_manager,
|
||||
'department' => $this->ldap_dept,
|
||||
'address' => $this->ldap_address,
|
||||
'city' => $this->ldap_city,
|
||||
'state' => $this->ldap_state,
|
||||
'zip' => $this->ldap_zip,
|
||||
'country' => $this->ldap_country,
|
||||
'location' => $this->ldap_location,
|
||||
'active_flag' => $this->ldap_active_flag,
|
||||
];
|
||||
}
|
||||
|
||||
protected function saveStep3(): void
|
||||
{
|
||||
$this->validate($this->step3SyntaxRules(), attributes: $this->step3SyntaxAttributes());
|
||||
@ -923,7 +933,7 @@ class LdapSettings extends Component
|
||||
// whole entry when we only care about a handful.
|
||||
$requestedAttrs = array_values(array_filter(array_map(
|
||||
fn ($attr) => trim(strtolower((string) $attr)),
|
||||
$this->attributeMap(),
|
||||
Ldap::attributeMap($this),
|
||||
)));
|
||||
|
||||
$searchResult = @ldap_search($conn, $this->ldap_basedn, $lookupFilter, $requestedAttrs);
|
||||
@ -986,10 +996,12 @@ class LdapSettings extends Component
|
||||
// muted). Attribute names are compared lowercase. LDAP is
|
||||
// case-insensitive on attribute names.
|
||||
$preview = [];
|
||||
foreach ($this->attributeMap() as $snipeField => $ldapAttr) {
|
||||
$labels = Ldap::attributeLabels();
|
||||
foreach (Ldap::attributeMap($this) as $snipeField => $ldapAttr) {
|
||||
$label = $labels[$snipeField] ?? $snipeField;
|
||||
$ldapAttrLower = trim(strtolower((string) $ldapAttr));
|
||||
if ($ldapAttrLower === '') {
|
||||
$preview[$snipeField] = ['attr' => null, 'value' => null];
|
||||
$preview[$snipeField] = ['label' => $label, 'attr' => null, 'value' => null];
|
||||
|
||||
continue;
|
||||
}
|
||||
@ -997,7 +1009,7 @@ class LdapSettings extends Component
|
||||
if (isset($attributes[$ldapAttrLower][0])) {
|
||||
$value = $attributes[$ldapAttrLower][0];
|
||||
}
|
||||
$preview[$snipeField] = ['attr' => $ldapAttr, 'value' => $value];
|
||||
$preview[$snipeField] = ['label' => $label, 'attr' => $ldapAttr, 'value' => $value];
|
||||
}
|
||||
|
||||
$this->step3TestDn = (string) $dn;
|
||||
@ -1394,6 +1406,20 @@ class LdapSettings extends Component
|
||||
|
||||
public function updated(string $property): void
|
||||
{
|
||||
// Read-only lock: in demo mode any mutation to a persisted
|
||||
// LDAP config field gets reverted to the seeded Setting value
|
||||
// before the rest of the updated() logic runs. Server-side
|
||||
// enforcement, so a client that fakes wire:model updates
|
||||
// around the UI's readonly / disabled attributes still can't
|
||||
// get modified creds into a Test Bind / Test Find User call.
|
||||
// test_sample_username stays writable so the Look Up preview
|
||||
// still works.
|
||||
if ($this->isReadOnly && ! in_array($property, self::READ_ONLY_ALLOWED_PROPS, true)) {
|
||||
$this->hydrateFromPersisted();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Trim string values on assignment so pasted-with-whitespace
|
||||
// inputs get normalized both in the visible field and in the
|
||||
// saved config. Without this a leading space on ldap_server
|
||||
|
||||
@ -60,8 +60,6 @@ class Ldap extends Model
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
*
|
||||
* @since [v3.0]
|
||||
*
|
||||
* @return connection
|
||||
*/
|
||||
public static function connectToLdap()
|
||||
{
|
||||
@ -180,16 +178,14 @@ class Ldap extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds/authenticates the user to LDAP, and returns their attributes.
|
||||
* Binds/authenticates the user to LDAP, and returns their attributes
|
||||
* (lowercase-keyed) on success or false when the bind or search fails.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
*
|
||||
* @since [v3.0]
|
||||
*
|
||||
* @param bool|false $user
|
||||
* @return bool true if the username and/or password provided are valid
|
||||
* false if the username and/or password provided are invalid
|
||||
* array of ldap_attributes if $user is true
|
||||
* @return array|false
|
||||
*/
|
||||
public static function findAndBindUserLdap($username, $password)
|
||||
{
|
||||
@ -311,52 +307,104 @@ class Ldap extends Model
|
||||
* @param $ldapatttibutes
|
||||
* @return array|bool
|
||||
*/
|
||||
/**
|
||||
* Single source of truth for the LDAP-attribute mapping. Internal
|
||||
* key (used across parseAndMapLdapAttributes' $item, the User field
|
||||
* writes in applyLdapAttributesToUser, and LdapSync's specific
|
||||
* lookups) => LDAP attribute name.
|
||||
*
|
||||
* Keys mirror the User model's column names (jobtitle,
|
||||
* employee_num, first_name, etc.) so downstream code can walk this
|
||||
* map and write straight onto a User without translation.
|
||||
*
|
||||
* `$source` defaults to the persisted Setting model so backend
|
||||
* callers (LdapSync, parseAndMapLdapAttributes, applyLdapAttributesToUser)
|
||||
* see saved values. The LDAP wizard Livewire component passes
|
||||
* `$this` so its live preview reflects in-flight form edits before
|
||||
* they're written to Settings. Any object exposing the same
|
||||
* `ldap_*` properties works.
|
||||
*
|
||||
* @param object|null $source Setting-shaped object; defaults to Setting::getSettings()
|
||||
* @return array<string, ?string>
|
||||
*/
|
||||
public static function attributeMap(?object $source = null): array
|
||||
{
|
||||
$source ??= Setting::getSettings();
|
||||
|
||||
return [
|
||||
'username' => $source->ldap_username_field,
|
||||
'first_name' => $source->ldap_fname_field,
|
||||
'last_name' => $source->ldap_lname_field,
|
||||
'employee_num' => $source->ldap_emp_num,
|
||||
'display_name' => $source->ldap_display_name,
|
||||
'email' => $source->ldap_email,
|
||||
'phone' => $source->ldap_phone_field,
|
||||
'mobile' => $source->ldap_mobile,
|
||||
'jobtitle' => $source->ldap_jobtitle,
|
||||
'address' => $source->ldap_address,
|
||||
'city' => $source->ldap_city,
|
||||
'state' => $source->ldap_state,
|
||||
'zip' => $source->ldap_zip,
|
||||
'country' => $source->ldap_country,
|
||||
'department' => $source->ldap_dept,
|
||||
'location' => $source->ldap_location,
|
||||
'manager' => $source->ldap_manager,
|
||||
// LdapSync-only: activated is consumed by the active-
|
||||
// directory sync logic in the console command, which reads
|
||||
// the mapped LDAP attribute (or AD's useraccountcontrol)
|
||||
// and writes the resulting bool onto user.activated.
|
||||
// parseAndMapLdapAttributes does not surface it because
|
||||
// the first-login path has no use for it (the user just
|
||||
// successfully bound to LDAP, they're active by definition).
|
||||
'activated' => $source->ldap_active_flag,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Companion to attributeMap(): internal key => translated human
|
||||
* label. Consumed by the LDAP wizard's step-3 preview table and by
|
||||
* the settings-page ldaptest results table, so both render
|
||||
* "Employee Number" / "Title" / etc. instead of the raw
|
||||
* snake_case internal keys. Adding a new key to attributeMap()
|
||||
* should be paired with an entry here so it shows up nicely in
|
||||
* both places automatically.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function attributeLabels(): array
|
||||
{
|
||||
return [
|
||||
'username' => trans('general.username'),
|
||||
'first_name' => trans('general.first_name'),
|
||||
'last_name' => trans('general.last_name'),
|
||||
'employee_num' => trans('general.employee_number'),
|
||||
'display_name' => trans('admin/users/table.display_name'),
|
||||
'email' => trans('general.email'),
|
||||
'phone' => trans('general.phone'),
|
||||
'mobile' => trans('admin/users/table.mobile'),
|
||||
'jobtitle' => trans('admin/users/table.title'),
|
||||
'address' => trans('general.address'),
|
||||
'city' => trans('general.city'),
|
||||
'state' => trans('general.state'),
|
||||
'zip' => trans('general.zip'),
|
||||
'country' => trans('general.country'),
|
||||
'department' => trans('general.department'),
|
||||
'location' => trans('general.location'),
|
||||
'manager' => trans('admin/users/table.manager'),
|
||||
'activated' => trans('admin/users/table.activated'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseAndMapLdapAttributes($ldapattributes)
|
||||
{
|
||||
// Get LDAP attribute config. The settings column names here are
|
||||
// the same ones LdapSync's $ldap_map reads, so this parser and
|
||||
// the bulk-sync command see identical field lookups.
|
||||
$settings = Setting::getSettings();
|
||||
$ldap_result_username = $settings->ldap_username_field;
|
||||
$ldap_result_emp_num = $settings->ldap_emp_num;
|
||||
$ldap_result_last_name = $settings->ldap_lname_field;
|
||||
$ldap_result_first_name = $settings->ldap_fname_field;
|
||||
$ldap_result_display_name = $settings->ldap_display_name;
|
||||
$ldap_result_email = $settings->ldap_email;
|
||||
$ldap_result_phone = $settings->ldap_phone_field;
|
||||
$ldap_result_mobile = $settings->ldap_mobile;
|
||||
$ldap_result_jobtitle = $settings->ldap_jobtitle;
|
||||
$ldap_result_address = $settings->ldap_address;
|
||||
$ldap_result_city = $settings->ldap_city;
|
||||
$ldap_result_state = $settings->ldap_state;
|
||||
$ldap_result_zip = $settings->ldap_zip;
|
||||
$ldap_result_country = $settings->ldap_country;
|
||||
$ldap_result_location = $settings->ldap_location;
|
||||
$ldap_result_dept = $settings->ldap_dept;
|
||||
$ldap_result_manager = $settings->ldap_manager;
|
||||
|
||||
// Get LDAP user data. Kept in the same shape LdapSync's per-user
|
||||
// $item array uses so the two paths stay comparable when this
|
||||
// one grows.
|
||||
$item = [];
|
||||
$item['username'] = $ldapattributes[$ldap_result_username][0] ?? '';
|
||||
$item['employee_number'] = $ldapattributes[$ldap_result_emp_num][0] ?? '';
|
||||
$item['lastname'] = $ldapattributes[$ldap_result_last_name][0] ?? '';
|
||||
$item['firstname'] = $ldapattributes[$ldap_result_first_name][0] ?? '';
|
||||
$item['display_name'] = $ldapattributes[$ldap_result_display_name][0] ?? '';
|
||||
$item['email'] = $ldapattributes[$ldap_result_email][0] ?? '';
|
||||
$item['telephone'] = $ldapattributes[$ldap_result_phone][0] ?? '';
|
||||
$item['mobile'] = $ldapattributes[$ldap_result_mobile][0] ?? '';
|
||||
$item['jobtitle'] = $ldapattributes[$ldap_result_jobtitle][0] ?? '';
|
||||
$item['address'] = $ldapattributes[$ldap_result_address][0] ?? '';
|
||||
$item['city'] = $ldapattributes[$ldap_result_city][0] ?? '';
|
||||
$item['state'] = $ldapattributes[$ldap_result_state][0] ?? '';
|
||||
$item['zip'] = $ldapattributes[$ldap_result_zip][0] ?? '';
|
||||
$item['country'] = $ldapattributes[$ldap_result_country][0] ?? '';
|
||||
$item['department'] = $ldapattributes[$ldap_result_dept][0] ?? '';
|
||||
$item['manager'] = $ldapattributes[$ldap_result_manager][0] ?? '';
|
||||
$item['location'] = $ldapattributes[$ldap_result_location][0] ?? '';
|
||||
$item['locale'] = app()->getLocale();
|
||||
foreach (self::attributeMap() as $key => $ldapAttr) {
|
||||
// activated is LdapSync's concern. See attributeMap().
|
||||
if ($key === 'activated') {
|
||||
continue;
|
||||
}
|
||||
$item[$key] = $ldapAttr ? ($ldapattributes[$ldapAttr][0] ?? '') : '';
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
@ -381,53 +429,53 @@ class Ldap extends Model
|
||||
*/
|
||||
public static function applyLdapAttributesToUser(User $user, array $ldapAttr): void
|
||||
{
|
||||
$settings = Setting::getSettings();
|
||||
$map = self::attributeMap();
|
||||
|
||||
// Always-written identity fields. These have no per-field gate
|
||||
// because Snipe-IT considers username / first name / last name /
|
||||
// email load-bearing for every user — if a mapping's blank the
|
||||
// email important for every user, if a mapping's blank the
|
||||
// LDAP payload just gives us an empty string, matching the
|
||||
// pre-fix behavior on the create path.
|
||||
$user->username = $ldapAttr['username'];
|
||||
$user->first_name = $ldapAttr['firstname'];
|
||||
$user->last_name = $ldapAttr['lastname'];
|
||||
$user->first_name = $ldapAttr['first_name'];
|
||||
$user->last_name = $ldapAttr['last_name'];
|
||||
$user->email = $ldapAttr['email'];
|
||||
|
||||
if ($settings->ldap_display_name != '') {
|
||||
if ($map['display_name'] != '') {
|
||||
$user->display_name = $ldapAttr['display_name'];
|
||||
}
|
||||
if ($settings->ldap_emp_num != '') {
|
||||
$user->employee_num = e($ldapAttr['employee_number']);
|
||||
if ($map['employee_num'] != '') {
|
||||
$user->employee_num = e($ldapAttr['employee_num']);
|
||||
}
|
||||
if ($settings->ldap_phone_field != '') {
|
||||
$user->phone = $ldapAttr['telephone'];
|
||||
if ($map['phone'] != '') {
|
||||
$user->phone = $ldapAttr['phone'];
|
||||
}
|
||||
if ($settings->ldap_mobile != '') {
|
||||
if ($map['mobile'] != '') {
|
||||
$user->mobile = $ldapAttr['mobile'];
|
||||
}
|
||||
if ($settings->ldap_jobtitle != '') {
|
||||
if ($map['jobtitle'] != '') {
|
||||
$user->jobtitle = $ldapAttr['jobtitle'];
|
||||
}
|
||||
if ($settings->ldap_address != '') {
|
||||
if ($map['address'] != '') {
|
||||
$user->address = $ldapAttr['address'];
|
||||
}
|
||||
if ($settings->ldap_city != '') {
|
||||
if ($map['city'] != '') {
|
||||
$user->city = $ldapAttr['city'];
|
||||
}
|
||||
if ($settings->ldap_state != '') {
|
||||
if ($map['state'] != '') {
|
||||
$user->state = $ldapAttr['state'];
|
||||
}
|
||||
if ($settings->ldap_zip != '') {
|
||||
if ($map['zip'] != '') {
|
||||
$user->zip = $ldapAttr['zip'];
|
||||
}
|
||||
if ($settings->ldap_country != '') {
|
||||
if ($map['country'] != '') {
|
||||
$user->country = $ldapAttr['country'];
|
||||
}
|
||||
if ($settings->ldap_dept != '' && $ldapAttr['department'] !== '') {
|
||||
if ($map['department'] != '' && $ldapAttr['department'] !== '') {
|
||||
$department = Department::firstOrCreate(['name' => $ldapAttr['department']]);
|
||||
$user->department_id = $department->id;
|
||||
}
|
||||
if ($settings->ldap_location != '' && $ldapAttr['location'] !== '') {
|
||||
if ($map['location'] != '' && $ldapAttr['location'] !== '') {
|
||||
$location = Location::firstOrCreate(['name' => $ldapAttr['location']]);
|
||||
$user->location_id = $location->id;
|
||||
}
|
||||
@ -455,7 +503,7 @@ class Ldap extends Model
|
||||
$user = new User;
|
||||
self::applyLdapAttributesToUser($user, $item);
|
||||
|
||||
$user->locale = $item['locale'];
|
||||
$user->locale = app()->getLocale();
|
||||
$user->password = $user->noPassword();
|
||||
if ($settings->ldap_pw_sync == '1') {
|
||||
$user->password = bcrypt($password);
|
||||
|
||||
@ -657,15 +657,28 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
|
||||
*/
|
||||
public function isDeletable()
|
||||
{
|
||||
|
||||
return Gate::allows('delete', $this)
|
||||
&& (($this->assets_count ?? $this->assets()->count()) === 0)
|
||||
&& $this->hasNoAssignmentBlockers()
|
||||
&& ($this->deleted_at == '');
|
||||
}
|
||||
|
||||
/**
|
||||
* The association-blocker half of isDeletable(): true only when the
|
||||
* user has no assigned assets / accessories / licenses / consumables
|
||||
* and isn't managing any users or locations. Split out from
|
||||
* isDeletable() so scripts running outside a request context,
|
||||
* Artisan `snipeit:ldap-sync --delete` flow in particular, can share
|
||||
* the exact same rule without needing an authenticated Gate user to
|
||||
* satisfy the delete-permission check.
|
||||
*/
|
||||
public function hasNoAssignmentBlockers(): bool
|
||||
{
|
||||
return (($this->assets_count ?? $this->assets()->count()) === 0)
|
||||
&& (($this->accessories_count ?? $this->accessories()->count()) === 0)
|
||||
&& (($this->licenses_count ?? $this->licenses()->count()) === 0)
|
||||
&& (($this->consumables_count ?? $this->consumables()->count()) === 0)
|
||||
&& (($this->manages_users_count ?? $this->managesUsers()->count()) === 0)
|
||||
&& (($this->manages_locations_count ?? $this->managedLocations()->count()) === 0)
|
||||
&& ($this->deleted_at == '');
|
||||
&& (($this->manages_locations_count ?? $this->managedLocations()->count()) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Database\Seeders;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SettingsSeeder extends Seeder
|
||||
@ -22,7 +23,33 @@ class SettingsSeeder extends Seeder
|
||||
$settings->label2_2d_type = 'QRCODE';
|
||||
$settings->default_currency = 'USD';
|
||||
$settings->brand = 2;
|
||||
$settings->ldap_enabled = 0;
|
||||
// Forumsys hosts a free public read-only LDAP directory
|
||||
// (ldap://ldap.forumsys.com) that's handy for exercising the
|
||||
// LDAP wizard against a real server without standing up your
|
||||
// own. Pre-filling the fields lets devs (and the demo site)
|
||||
// click through Admin > Settings > LDAP and use the Test
|
||||
// Bind / Test Find User previews end to end. LDAP is enabled
|
||||
// so the wizard's return-visitor branch unlocks all steps
|
||||
// for demo visitors. The login controller separately skips
|
||||
// the LDAP auth branch when the app is in demo mode
|
||||
// (config('app.lock_passwords')), so demo logins never
|
||||
// actually hit Forumsys.
|
||||
$settings->ldap_enabled = '1';
|
||||
$settings->ldap_server = 'ldap://ldap.forumsys.com';
|
||||
$settings->is_ad = false;
|
||||
$settings->ldap_tls = false;
|
||||
$settings->ldap_server_cert_ignore = false;
|
||||
$settings->ldap_client_tls_cert = null;
|
||||
$settings->ldap_client_tls_key = null;
|
||||
$settings->ldap_basedn = 'dc=example,dc=com';
|
||||
$settings->ldap_uname = 'cn=read-only-admin,dc=example,dc=com';
|
||||
$settings->ldap_pword = Crypt::encrypt('password');
|
||||
$settings->ldap_filter = '';
|
||||
$settings->ldap_auth_filter_query = 'uid=';
|
||||
$settings->ldap_username_field = 'uid';
|
||||
$settings->ldap_fname_field = 'cn';
|
||||
$settings->ldap_lname_field = 'sn';
|
||||
$settings->ldap_email = 'mail';
|
||||
$settings->full_multiple_companies_support = 0;
|
||||
$settings->label2_1d_type = 'C128';
|
||||
$settings->skin = 'blue';
|
||||
|
||||
@ -306,24 +306,12 @@ parameters:
|
||||
count: 1
|
||||
path: app/Console/Commands/LdapSync.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Models\\User\:\:\$display_name\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Console/Commands/LdapSync.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\Relation\:\:attach\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: app/Console/Commands/LdapSync.php
|
||||
|
||||
-
|
||||
message: '#^Call to function array_key_exists\(\) with ''id'' and \*NEVER\* will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: app/Console/Commands/LdapSync.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_array\(\) with App\\Models\\Location will always evaluate to false\.$#'
|
||||
identifier: function.impossibleType
|
||||
@ -351,7 +339,7 @@ parameters:
|
||||
-
|
||||
message: '#^Result of && is always false\.$#'
|
||||
identifier: booleanAnd.alwaysFalse
|
||||
count: 2
|
||||
count: 1
|
||||
path: app/Console/Commands/LdapSync.php
|
||||
|
||||
-
|
||||
@ -360,12 +348,6 @@ parameters:
|
||||
count: 2
|
||||
path: app/Console/Commands/LdapSync.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$location in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 1
|
||||
path: app/Console/Commands/LdapSync.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Console\\Commands\\LdapTroubleshooter\:\:\$settings\.$#'
|
||||
identifier: property.notFound
|
||||
|
||||
@ -28,8 +28,6 @@
|
||||
name="company_id"
|
||||
:selected="old('company_id', $item->company_id)"
|
||||
/>
|
||||
@else
|
||||
<input id="hidden_company_id" type="hidden" name="company_id" value="{{ Auth::user()->company_id }}">
|
||||
@endif
|
||||
|
||||
<x-form.row
|
||||
|
||||
@ -212,9 +212,6 @@
|
||||
|
||||
<div class="box-body">
|
||||
|
||||
<span id="wizard-locked-note" class="sr-only">
|
||||
{{ trans('admin/settings/general.ldap_wizard.locked_help') }}
|
||||
</span>
|
||||
|
||||
{{-- Step title + help text, always the current step's copy. --}}
|
||||
@php
|
||||
@ -230,10 +227,22 @@
|
||||
<x-form.legend for="{{ $currentStep }}" help_text="{!! trans($stepHelpKey) !!}" />
|
||||
@endif
|
||||
|
||||
@if ($isReadOnly)
|
||||
<x-alert type="warning" role="status" icon="warning">
|
||||
This is a demo. Every LDAP config field is read-only, but you can still <strong><a href="?step=3">enter
|
||||
a sample username</a></strong> on step 3 and use the Test Find User button to see the wizard
|
||||
search against the pre-seeded readonly directory. (You can search on tesla, einstein, or curie.)
|
||||
The wizard will not actually save any LDAP settings in this demo.
|
||||
</x-alert>
|
||||
|
||||
@endif
|
||||
|
||||
{{-- Wizard progress indicator. Same .bs-wizard class the
|
||||
quickstart setup layout + importer modal use. Flex + flex:1 on
|
||||
children rather than bootstrap col-md-*, so the layout
|
||||
stays uniform regardless of step count. --}}
|
||||
|
||||
|
||||
<div class="bs-wizard" style="border-bottom:0; margin-bottom: 25px; display: flex;" role="group" aria-label="{{ trans('admin/settings/general.ldap_wizard.progress_label') }}">
|
||||
@foreach ($this->stepTitles as $stepNum => $stepTitle)
|
||||
@php
|
||||
@ -344,6 +353,7 @@
|
||||
wire:model.live="is_ad"
|
||||
:label="trans('admin/settings/general.ad')"
|
||||
:checked="$is_ad"
|
||||
:disabled="$isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- AD Domain (only when is_ad is checked) -->
|
||||
@ -359,6 +369,7 @@
|
||||
wire:model="ad_domain"
|
||||
placeholder="{{ trans('general.example').'example.com' }}"
|
||||
:required="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -376,6 +387,7 @@
|
||||
wire:model.live.debounce.500ms="ldap_server"
|
||||
placeholder="{{ trans('general.example').'ldap://ldap.example.com' }}"
|
||||
:required="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -387,6 +399,7 @@
|
||||
:label="trans('admin/settings/general.ldap_tls')"
|
||||
:checked="$ldap_tls"
|
||||
help_text="{!! trans('admin/settings/general.ldap_tls_help') !!}"
|
||||
:disabled="$isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- Ignore LDAP certificate -->
|
||||
@ -396,6 +409,7 @@
|
||||
:label="trans('admin/settings/general.ldap_server_cert_ignore')"
|
||||
:checked="$ldap_server_cert_ignore"
|
||||
help_text="{!! trans('admin/settings/general.ldap_server_cert_help') !!}"
|
||||
:disabled="$isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- Client TLS key -->
|
||||
@ -410,6 +424,7 @@
|
||||
rows="4"
|
||||
:placeholder="sprintf('%s-----BEGIN RSA PRIVATE KEY-----%s1234567890%s-----END RSA PRIVATE KEY-----', trans('general.example'), PHP_EOL, PHP_EOL)"
|
||||
:required="$ldap_client_tls_cert !== ''"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -427,6 +442,7 @@
|
||||
rows="4"
|
||||
:placeholder="sprintf('%s-----BEGIN CERTIFICATE-----%s1234567890%s-----END CERTIFICATE-----', trans('general.example'), PHP_EOL, PHP_EOL)"
|
||||
:required="$ldap_client_tls_key !== ''"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -452,6 +468,7 @@
|
||||
placeholder="{{ trans('general.example').'ou=users,dc=example,dc=com' }}"
|
||||
:required="true"
|
||||
:ignore-autofill="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -471,6 +488,7 @@
|
||||
placeholder="{{ trans('general.example').($is_ad ? 'admin@example.com' : 'cn=admin,dc=example,dc=com') }}"
|
||||
:ignore-autofill="true"
|
||||
:required="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -487,6 +505,7 @@
|
||||
wire:model.live.debounce.500ms="ldap_pword"
|
||||
:required="true"
|
||||
:ignore-autofill="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -503,6 +522,7 @@
|
||||
wire:model.live.debounce.500ms="ldap_filter"
|
||||
placeholder="{{ trans('general.example').'&(cn=*)' }}"
|
||||
:ignore-autofill="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -520,6 +540,7 @@
|
||||
placeholder="{{ trans('general.example').'uid=' }}"
|
||||
:required="true"
|
||||
:ignore-autofill="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -553,6 +574,7 @@
|
||||
:placeholder="$placeholderExample !== '' ? trans('general.example').$placeholderExample : ''"
|
||||
:required="$required"
|
||||
:ignore-autofill="true"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -568,6 +590,7 @@
|
||||
:label="trans('admin/settings/general.ldap_invert_active_flag')"
|
||||
:checked="$ldap_invert_active_flag"
|
||||
help_text="{!! trans('admin/settings/general.ldap_invert_active_flag_help') !!}"
|
||||
:disabled="$isReadOnly"
|
||||
/>
|
||||
|
||||
{{-- Sample-lookup section, boxed in an x-well so it
|
||||
@ -646,7 +669,7 @@
|
||||
<tbody>
|
||||
@foreach ($step3TestAttributes as $snipeField => $preview)
|
||||
<tr>
|
||||
<td>{{ $snipeField }}</td>
|
||||
<td>{{ $preview['label'] ?? $snipeField }}</td>
|
||||
<td>
|
||||
@if ($preview['attr'])
|
||||
<code>{{ $preview['attr'] }}</code>
|
||||
@ -683,6 +706,7 @@
|
||||
:label="trans('admin/settings/general.ldap_wizard.sync.ldap_pw_sync_label')"
|
||||
:checked="$ldap_pw_sync"
|
||||
help_text="{!! trans('admin/settings/general.ldap_pw_sync_help') !!}"
|
||||
:disabled="$isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- Default permissions group -->
|
||||
@ -701,6 +725,7 @@
|
||||
] + $this->permissionGroups"
|
||||
:forLivewire="true"
|
||||
style="width: 100%"
|
||||
:disabled="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -717,6 +742,7 @@
|
||||
name="custom_forgot_pass_url"
|
||||
wire:model.blur="custom_forgot_pass_url"
|
||||
placeholder="{{ trans('general.example').'https://my.ldapserver-forgotpass.com' }}"
|
||||
:readonly="$isReadOnly"
|
||||
/>
|
||||
</x-slot:input>
|
||||
</x-form.row>
|
||||
@ -762,7 +788,6 @@
|
||||
<strong>{{ trans('admin/settings/general.ldap_wizard.verifying_help') }}</strong>
|
||||
</p>
|
||||
|
||||
<x-demo-lock>{{ trans('general.feature_disabled') }}</x-demo-lock>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -789,7 +814,7 @@
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="saveAndAdvance"
|
||||
class="btn btn-primary"
|
||||
@disabled(config('app.lock_passwords') || ! $this->canAdvance)
|
||||
@disabled(! config('app.lock_passwords') && ! $this->canAdvance)
|
||||
>
|
||||
<span wire:loading.remove wire:target="saveAndAdvance">
|
||||
@if ($currentStep === 4)
|
||||
|
||||
@ -852,60 +852,32 @@
|
||||
html += '<div style="overflow:auto;">'
|
||||
html += '<div>{{ trans('admin/settings/message.ldap.sync_success') }}<br><br></div>'
|
||||
html += '<table class="table table-striped snipe-table table-bordered table-condensed">'
|
||||
html += buildLdapResultsTableHeader()
|
||||
html += buildLdapResultsTableBody(results.user_sync.users)
|
||||
html += buildLdapResultsTableHeader(results.user_sync.fields)
|
||||
html += buildLdapResultsTableBody(results.user_sync.users, results.user_sync.fields)
|
||||
html += '</table></div>'
|
||||
html += ''
|
||||
return html;
|
||||
}
|
||||
|
||||
function buildLdapResultsTableHeader(user)
|
||||
function buildLdapResultsTableHeader(fields)
|
||||
{
|
||||
var keys = [
|
||||
'{{ trans('admin/settings/general.employee_number') }}',
|
||||
'{{ trans('mail.username') }}',
|
||||
'{{ trans('admin/users/table.display_name') }}',
|
||||
'{{ trans('general.first_name') }}',
|
||||
'{{ trans('general.last_name') }}',
|
||||
'{{ trans('general.email') }}',
|
||||
'{{ trans('general.phone') }}',
|
||||
'{{ trans('admin/users/table.mobile') }}',
|
||||
'{{ trans('admin/users/table.manager') }}',
|
||||
'{{ trans('general.address') }}',
|
||||
'{{ trans('general.city') }}',
|
||||
'{{ trans('general.state') }}',
|
||||
'{{ trans('general.zip') }}',
|
||||
'{{ trans('general.country') }}',
|
||||
'{{ trans('general.location') }}',
|
||||
]
|
||||
let header = '<thead><tr>'
|
||||
for (var i in keys) {
|
||||
header += '<th scope="col" style="white-space: nowrap;">' + keys[i] + '</th>'
|
||||
for (const key in fields) {
|
||||
header += '<th scope="col" style="white-space: nowrap;">' + fields[key] + '</th>'
|
||||
}
|
||||
header += "</tr></thead>"
|
||||
return header;
|
||||
}
|
||||
|
||||
function buildLdapResultsTableBody(users)
|
||||
function buildLdapResultsTableBody(users, fields)
|
||||
{
|
||||
let body = '<tbody>'
|
||||
for (var i in users) {
|
||||
body += '<tr>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].employee_number ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].username ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].display_name ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].firstname ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].lastname ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].email ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].phone ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].mobile ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;"><span class="nullval">' + (users[i].manager ?? 'NULL') + '</span></td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].address ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].city ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].state ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].zip ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].country ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
body += '<td style="white-space: nowrap;">' + (users[i].location ?? '<span class="nullval">NULL</span>') + '</td>';
|
||||
const nullCell = '<span class="nullval">NULL</span>'
|
||||
for (const i in users) {
|
||||
body += '<tr>'
|
||||
for (const key in fields) {
|
||||
body += '<td style="white-space: nowrap;">' + (users[i][key] ?? nullCell) + '</td>'
|
||||
}
|
||||
body += '</tr>'
|
||||
}
|
||||
body += "</tbody>"
|
||||
|
||||
@ -18,11 +18,12 @@
|
||||
|
||||
@section('header_right')
|
||||
|
||||
@can('create', \App\Models\User::class)
|
||||
@if ($snipeSettings->ldap_enabled == 1)
|
||||
<a href="{{ route('ldap/user') }}" class="btn btn-theme pull-right"><i class="fas fa-sitemap"></i> {{trans('general.ldap_sync')}}</a>
|
||||
@endif
|
||||
@endcan
|
||||
{{-- LDAP sync surfaces users from across the entire directory
|
||||
regardless of company scoping, so the button is superuser-only
|
||||
to match LDAPImportController's authorization. --}}
|
||||
@if (auth()->user()?->isSuperUser() && $snipeSettings->ldap_enabled == 1)
|
||||
<a href="{{ route('ldap/user') }}" class="btn btn-theme pull-right"><i class="fas fa-sitemap"></i> {{trans('general.ldap_sync')}}</a>
|
||||
@endif
|
||||
@stop
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@ -8,110 +8,99 @@
|
||||
{{-- Page content --}}
|
||||
@section('content')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
@if ($snipeSettings->ldap_enabled == 0)
|
||||
{{ trans('admin/users/message.ldap_not_configured') }}
|
||||
@else
|
||||
<x-container columns="1" class="col-md-8 col-md-offset-2">
|
||||
@if ($snipeSettings->ldap_enabled == 0)
|
||||
{{ trans('admin/users/message.ldap_not_configured') }}
|
||||
@else
|
||||
<x-form id="ldap-form">
|
||||
<x-box>
|
||||
<x-callout type="legend" icon="tip" class="col-md-12">
|
||||
|
||||
<div class="box box-default">
|
||||
<form class="form-horizontal" role="form" method="post" action="" id="ldap-form">
|
||||
{{csrf_field()}}
|
||||
<div class="box-body">
|
||||
<div class="callout callout-legend col-md-12">
|
||||
<p>
|
||||
<i class="fa-solid fa-lightbulb text-info"></i>
|
||||
<strong>
|
||||
{!! trans('admin/users/general.ldap_sync_intro', ['link' => 'https://snipe-it.readme.io/docs/ldap-sync#/']) !!}
|
||||
</strong>
|
||||
</p>
|
||||
<strong>
|
||||
{!! trans('admin/users/general.ldap_sync_intro', ['link' => 'https://snipe-it.readme.io/docs/ldap-sync#/']) !!}
|
||||
</strong>
|
||||
|
||||
</x-callout>
|
||||
|
||||
<x-input.location-select
|
||||
:label="trans('general.ldap_sync_location')"
|
||||
name="location_id[]"
|
||||
:selected="null"
|
||||
:multiple="true"
|
||||
:hide-new-button="true"
|
||||
:help-text="trans('admin/users/general.ldap_config_text')"
|
||||
/>
|
||||
|
||||
<x-slot:customfooter>
|
||||
<div class="box-footer">
|
||||
<div class="text-left col-md-6">
|
||||
<a class="btn btn-link" href="{{ route('users.index') }}">{{ trans('button.cancel') }}</a>
|
||||
</div>
|
||||
<!-- Location -->
|
||||
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.ldap_sync_location'), 'help_text' => trans('admin/users/general.ldap_config_text'), 'fieldname' => 'location_id[]', 'multiple' => true])
|
||||
|
||||
</div><!-- ./box-body -->
|
||||
|
||||
<div class="box-footer">
|
||||
<div class="text-left col-md-6">
|
||||
<a class="btn btn-link" href="{{ route('users.index') }}">{{ trans('button.cancel') }}</a>
|
||||
</div>
|
||||
<div class="text-right col-md-6">
|
||||
<button type="submit" class="btn btn-primary" id="sync">
|
||||
<i id="sync-button-icon" class="fas fa-sync-alt icon-white" aria-hidden="true"></i> <span id="sync-button-text">{{ trans('general.synchronize') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div> <!-- ./box-footer -->
|
||||
</form>
|
||||
</div><!-- /.box -->
|
||||
|
||||
@endif
|
||||
</div><!-- /.col-md-8 -->
|
||||
</div><!-- /.row -->
|
||||
<div class="text-right col-md-6">
|
||||
<x-input.button class="btn-primary" id="sync">
|
||||
<i id="sync-button-icon" class="fas fa-sync-alt icon-white" aria-hidden="true"></i>
|
||||
<span id="sync-button-text">{{ trans('general.synchronize') }}</span>
|
||||
</x-input.button>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:customfooter>
|
||||
</x-box>
|
||||
</x-form>
|
||||
@endif
|
||||
</x-container>
|
||||
|
||||
@if (Session::get('summary'))
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h2 class="box-title">
|
||||
{{ trans('general.sync_results') }}
|
||||
</h2>
|
||||
</div><!-- /.box-header -->
|
||||
|
||||
<div class="box-body"><!-- .box-body -->
|
||||
<table
|
||||
data-cookie-id-table="ldapUserSync"
|
||||
data-id-table="ldapUserSyncTable"
|
||||
data-side-pagination="client"
|
||||
data-sort-order="asc"
|
||||
data-sort-name="username"
|
||||
data-show-refresh="false"
|
||||
id="customFieldsTable"
|
||||
data-advanced-search="false"
|
||||
class="table table-striped snipe-table"
|
||||
data-export-options='{
|
||||
"fileName": "ldap-sync-results-{{ date('Y-m-d') }}"
|
||||
}'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-sortable="true" data-visible="false" data-searchable="true">{{ trans('general.id') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.username') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('admin/users/table.display_name') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.employee_number') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.first_name') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.last_name') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.email') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.notes') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@foreach (Session::get('summary') as $entry)
|
||||
<tr>
|
||||
<td>{{ (array_key_exists('id', $entry)) ? $entry['id'] : '' }}</td>
|
||||
<td>{{ $entry['username'] }}</td>
|
||||
<td>{{ $entry['display_name'] }}</td>
|
||||
<td>{{ $entry['employee_number'] }}</td>
|
||||
<td>{{ $entry['firstname'] }}</td>
|
||||
<td>{{ $entry['lastname'] }}</td>
|
||||
<td>{{ $entry['email'] }}</td>
|
||||
<td>
|
||||
@if ($entry['status']=='success')
|
||||
<span class="text-success"><i class="fas fa-check"></i> {!! $entry['note'] !!}</span>
|
||||
@else
|
||||
<span class="alert-msg" role="alert" aria-live="assertive">{!! $entry['note'] !!}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!-- /.box-body -->
|
||||
</div><!-- /.box -->
|
||||
</div><!-- /.col-md-12 -->
|
||||
</div><!-- /.row -->
|
||||
<x-container columns="1" class="col-md-8 col-md-offset-2">
|
||||
<x-box :header="trans('general.sync_results')">
|
||||
<table
|
||||
data-cookie-id-table="ldapUserSync"
|
||||
data-id-table="ldapUserSyncTable"
|
||||
data-side-pagination="client"
|
||||
data-sort-order="asc"
|
||||
data-sort-name="username"
|
||||
data-show-refresh="false"
|
||||
id="customFieldsTable"
|
||||
data-advanced-search="false"
|
||||
class="table table-striped snipe-table"
|
||||
data-export-options='{
|
||||
"fileName": "ldap-sync-results-{{ date('Y-m-d') }}"
|
||||
}'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-sortable="true" data-visible="false" data-searchable="true">{{ trans('general.id') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.username') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('admin/users/table.display_name') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.employee_number') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.first_name') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.last_name') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.email') }}</th>
|
||||
<th scope="col" data-sortable="true" data-visible="true" data-searchable="true">{{ trans('general.notes') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@foreach (Session::get('summary') as $entry)
|
||||
<tr>
|
||||
<td>{{ (array_key_exists('id', $entry)) ? $entry['id'] : '' }}</td>
|
||||
<td>{{ $entry['username'] }}</td>
|
||||
<td>{{ $entry['display_name'] }}</td>
|
||||
<td>{{ $entry['employee_num'] }}</td>
|
||||
<td>{{ $entry['first_name'] }}</td>
|
||||
<td>{{ $entry['last_name'] }}</td>
|
||||
<td>{{ $entry['email'] }}</td>
|
||||
<td>
|
||||
@if ($entry['status']=='success')
|
||||
<span class="text-success"><i class="fas fa-check"></i> {!! $entry['note'] !!}</span>
|
||||
@else
|
||||
<span class="alert-msg" role="alert" aria-live="assertive">{!! $entry['note'] !!}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</x-box>
|
||||
</x-container>
|
||||
@endif
|
||||
|
||||
@stop
|
||||
|
||||
@ -95,8 +95,8 @@
|
||||
@endphp
|
||||
<div id="start_of_user_section"> {{-- used for page breaks when printing --}}</div>
|
||||
<h3>
|
||||
@if ($show_user->company)
|
||||
<b>{{ trans('admin/companies/table.name') }}:</b> {{ $show_user->company->name }}
|
||||
@if ($show_user->companies->isNotEmpty())
|
||||
<b>{{ trans('admin/companies/table.name') }}:</b> {{ $show_user->companies->pluck('name')->join(', ') }}
|
||||
<br>
|
||||
@endif
|
||||
{{ trans('general.assigned_to', ['name' => $show_user->display_name]) }}
|
||||
|
||||
Reference in New Issue
Block a user