diff --git a/.upgrade_requirements.json b/.upgrade_requirements.json new file mode 100644 index 0000000000..6fe066f8ea --- /dev/null +++ b/.upgrade_requirements.json @@ -0,0 +1,10 @@ +{ + "DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally", + "DOC2": "In other words, what you see locally are the requirements for your _current_ install", + "DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version", + "DOC4": "You should really just ignore it and run upgrade.php. Really", + "php_min_version": "7.4.0", + "php_max_major_minor": "8.1", + "php_max_wontwork": "8.2.0", + "current_snipeit_version": "6.3" +} diff --git a/app/Console/Commands/SamlClearExpiredNonces.php b/app/Console/Commands/SamlClearExpiredNonces.php new file mode 100644 index 0000000000..f03b55095e --- /dev/null +++ b/app/Console/Commands/SamlClearExpiredNonces.php @@ -0,0 +1,44 @@ +delete(); + return 0; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 0b80d2eccd..8d512f303b 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -25,6 +25,7 @@ class Kernel extends ConsoleKernel $schedule->command('backup:clean')->daily(); $schedule->command('snipeit:upcoming-audits')->daily(); $schedule->command('auth:clear-resets')->everyFifteenMinutes(); + $schedule->command('saml:clear_expired_nonces')->weekly(); } /** diff --git a/app/Http/Controllers/AssetModelsController.php b/app/Http/Controllers/AssetModelsController.php index 5ac958a8ac..012f40e399 100755 --- a/app/Http/Controllers/AssetModelsController.php +++ b/app/Http/Controllers/AssetModelsController.php @@ -288,7 +288,7 @@ class AssetModelsController extends Controller public function show($modelId = null) { $this->authorize('view', AssetModel::class); - $model = AssetModel::withTrashed()->find($modelId); + $model = AssetModel::withTrashed()->withCount('assets')->find($modelId); if (isset($model->id)) { return view('models/view', compact('model')); diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 2947344a50..158f318133 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -49,15 +49,86 @@ class BulkAssetsController extends Controller return redirect()->back()->with('error', trans('admin/hardware/message.update.no_assets_selected')); } + $asset_ids = $request->input('ids'); + // Figure out where we need to send the user after the update is complete, and store that in the session $bulk_back_url = request()->headers->get('referer'); session(['bulk_back_url' => $bulk_back_url]); + $allowed_columns = [ + 'id', + 'name', + 'asset_tag', + 'serial', + 'model_number', + 'last_checkout', + 'notes', + 'expected_checkin', + 'order_number', + 'image', + 'assigned_to', + 'created_at', + 'updated_at', + 'purchase_date', + 'purchase_cost', + 'last_audit_date', + 'next_audit_date', + 'warranty_months', + 'checkout_counter', + 'checkin_counter', + 'requests_counter', + 'byod', + 'asset_eol_date', + ]; - $asset_ids = $request->input('ids'); - // Using the 'short-ternary' A/K/A "Elvis operator" '?:' here because ->input() might return an empty string - list($sortname,$sortdir) = explode(" ",$request->input('sort') ?: 'id ASC'); - $assets = Asset::with('assignedTo', 'location', 'model')->whereIn('id', $asset_ids)->orderBy($sortname, $sortdir)->get(); + + /** + * Make sure the column is allowed, and if it's a custom field, make sure we strip the custom_fields. prefix + */ + $order = $request->input('order') === 'asc' ? 'asc' : 'desc'; + $sort_override = str_replace('custom_fields.', '', $request->input('sort')); + + // This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array) + $column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id'; + + $assets = Asset::with('assignedTo', 'location', 'model')->whereIn('assets.id', $asset_ids); + + switch ($sort_override) { + case 'model': + $assets->OrderModels($order); + break; + case 'model_number': + $assets->OrderModelNumber($order); + break; + case 'category': + $assets->OrderCategory($order); + break; + case 'manufacturer': + $assets->OrderManufacturer($order); + break; + case 'company': + $assets->OrderCompany($order); + break; + case 'location': + $assets->OrderLocation($order); + case 'rtd_location': + $assets->OrderRtdLocation($order); + break; + case 'status_label': + $assets->OrderStatus($order); + break; + case 'supplier': + $assets->OrderSupplier($order); + break; + case 'assigned_to': + $assets->OrderAssigned($order); + break; + default: + $assets->orderBy($column_sort, $order); + break; + } + + $assets = $assets->get(); $models = $assets->unique('model_id'); $modelNames = []; diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 100eed12b9..896ca11ff5 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Models\SamlNonce; use App\Models\Setting; use App\Models\User; use App\Models\Ldap; @@ -109,7 +110,14 @@ class LoginController extends Controller try { $user = $saml->samlLogin($samlData); - + $notValidAfter = new \Carbon\Carbon(@$samlData['assertionNotOnOrAfter']); + if(\Carbon::now()->greaterThanOrEqualTo($notValidAfter)) { + abort(400,"Expired SAML Assertion"); + } + if(SamlNonce::where('nonce', @$samlData['nonce'])->count() > 0) { + abort(400,"Assertion has already been used"); + } + Log::debug("okay, fine, this is a new nonce then. Good for you."); if (!is_null($user)) { Auth::login($user); } else { @@ -123,10 +131,14 @@ class LoginController extends Controller $user->last_login = \Carbon::now(); $user->saveQuietly(); } - + $s = new SamlNonce(); + $s->nonce = @$samlData['nonce']; + $s->not_valid_after = $notValidAfter; + $s->save(); + } catch (\Exception $e) { \Log::debug('There was an error authenticating the SAML user: '.$e->getMessage()); - throw new \Exception($e->getMessage()); + throw $e; } // Fallthrough with better logging diff --git a/app/Http/Livewire/SlackSettingsForm.php b/app/Http/Livewire/SlackSettingsForm.php index a5c3dffd3d..ee63e7a48f 100644 --- a/app/Http/Livewire/SlackSettingsForm.php +++ b/app/Http/Livewire/SlackSettingsForm.php @@ -16,7 +16,6 @@ class SlackSettingsForm extends Component public $isDisabled ='disabled' ; public $webhook_name; public $webhook_link; - public $webhook_test; public $webhook_placeholder; public $webhook_icon; public $webhook_selected; @@ -43,35 +42,32 @@ class SlackSettingsForm extends Component "icon" => 'fab fa-slack', "placeholder" => "https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXXXXX", "link" => 'https://api.slack.com/messaging/webhooks', - "test" => "testWebhook" ), - "general" => array( + "general"=> array( "name" => trans('admin/settings/general.general_webhook'), "icon" => "fab fa-hashtag", - "placeholder" => "", + "placeholder" => trans('general.url'), "link" => "", - "test" => "testWebhook" ), - "google" => array( - "name" => trans('admin/settings/general.google_workspaces'), - "icon" => "fa-brands fa-google", - "placeholder" => "https://chat.googleapis.com/v1/spaces/xxxxxxxx/messages?key=xxxxxx", - "link" => "https://developers.google.com/chat/how-tos/webhooks#register_the_incoming_webhook", - "test" => "googleWebhookTest" + "microsoft" => array( + "name" => trans('admin/settings/general.ms_teams'), + "icon" => "fa-brands fa-microsoft", + "placeholder" => "https://abcd.webhook.office.com/webhookb2/XXXXXXX", + "link" => "https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet#create-incoming-webhooks-1", ), ]; $this->setting = Setting::getSettings(); $this->save_button = trans('general.save'); $this->webhook_selected = $this->setting->webhook_selected; - $this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"]; $this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"]; $this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"]; + $this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"]; + $this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"]; $this->webhook_endpoint = $this->setting->webhook_endpoint; $this->webhook_channel = $this->setting->webhook_channel; $this->webhook_botname = $this->setting->webhook_botname; $this->webhook_options = $this->setting->webhook_selected; - $this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"]; if($this->setting->webhook_endpoint != null && $this->setting->webhook_channel != null){ @@ -89,11 +85,13 @@ class SlackSettingsForm extends Component $this->webhook_name = $this->webhook_text[$this->webhook_selected]['name']; $this->webhook_icon = $this->webhook_text[$this->webhook_selected]["icon"]; ; $this->webhook_placeholder = $this->webhook_text[$this->webhook_selected]["placeholder"]; + $this->webhook_endpoint = null; $this->webhook_link = $this->webhook_text[$this->webhook_selected]["link"]; if($this->webhook_selected != 'slack'){ $this->isDisabled= ''; $this->save_button = trans('general.save'); } + } private function isButtonDisabled() { @@ -152,31 +150,7 @@ class SlackSettingsForm extends Component return session()->flash('error' , trans('admin/settings/message.webhook.error_misc')); } - public function googleWebhookTest(){ - $url = $this->webhook_endpoint; - $data = json_encode([ 'text' => trans('general.webhook_test_msg', ['app' => $this->webhook_name])]); - $headers = [ - 'Authorization' => 'Bearer'. 'AIzaSyBu-61gEOhYGfrmT3fHQj6vS8TDWpo1B5U', - 'Content-Type' => 'application/json', - ]; - $client = new Client(); - try { - $response = $client->post($url,[ - 'headers' => $headers, - 'json' => $data, - ]); - - if (($response->getStatusCode() == 302) || ($response->getStatusCode() == 301)) { - return session()->flash('error', trans('admin/settings/message.webhook.error_redirect', ['endpoint' => $this->webhook_endpoint])); - } - } catch (\Exception $e) { - - $this->isDisabled='disabled'; - $this->save_button = trans('admin/settings/general.webhook_presave'); - return session()->flash('error' , trans('admin/settings/message.webhook.error', ['error_message' => $e->getMessage(), 'app' => $this->webhook_name])); - } - } public function clearSettings(){ @@ -211,8 +185,40 @@ class SlackSettingsForm extends Component $this->setting->save(); session()->flash('success',trans('admin/settings/message.update.success')); - } } + public function msTeamTestWebhook(){ + + $payload = + [ + "@type" => "MessageCard", + "@context" => "http://schema.org/extensions", + "summary" => trans('mail.snipe_webhook_summary'), + "title" => trans('mail.snipe_webhook_test'), + "text" => trans('general.webhook_test_msg', ['app' => $this->webhook_name]), + ]; + + try { + $response = Http::withHeaders([ + 'content-type' => 'applications/json', + ])->post($this->webhook_endpoint, + $payload)->throw(); + + if(($response->getStatusCode() == 302)||($response->getStatusCode() == 301)){ + return session()->flash('error' , trans('admin/settings/message.webhook.error_redirect', ['endpoint' => $this->webhook_endpoint])); + } + $this->isDisabled=''; + $this->save_button = trans('general.save'); + return session()->flash('success' , trans('admin/settings/message.webhook.success', ['webhook_name' => $this->webhook_name])); + + } catch (\Exception $e) { + + $this->isDisabled='disabled'; + $this->save_button = trans('admin/settings/general.webhook_presave'); + return session()->flash('error' , trans('admin/settings/message.webhook.error', ['error_message' => $e->getMessage(), 'app' => $this->webhook_name])); + } + + return session()->flash('error' , trans('admin/settings/message.webhook.error_misc')); + } } diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 17a8a6c1bf..162b07d276 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -58,8 +58,13 @@ class CheckoutableListener } if ($this->shouldSendWebhookNotification()) { - Notification::route('slack', Setting::getSettings()->webhook_endpoint) - ->notify($this->getCheckoutNotification($event)); + + //slack doesn't include the url in its messaging format so this is needed to hit the endpoint + if(Setting::getSettings()->webhook_selected =='slack') { + + Notification::route('slack', Setting::getSettings()->webhook_endpoint) + ->notify($this->getCheckoutNotification($event)); + } } } catch (ClientException $e) { Log::debug("Exception caught during checkout notification: " . $e->getMessage()); @@ -107,11 +112,15 @@ class CheckoutableListener $this->getCheckinNotification($event) ); } + //slack doesn't include the url in its messaging format so this is needed to hit the endpoint + if(Setting::getSettings()->webhook_selected =='slack') { - if ($this->shouldSendWebhookNotification()) { - Notification::route('slack', Setting::getSettings()->webhook_endpoint) - ->notify($this->getCheckinNotification($event)); + if ($this->shouldSendWebhookNotification()) { + Notification::route('slack', Setting::getSettings()->webhook_endpoint) + ->notify($this->getCheckinNotification($event)); + } } + } catch (ClientException $e) { Log::debug("Exception caught during checkout notification: " . $e->getMessage()); } catch (Exception $e) { @@ -216,9 +225,10 @@ class CheckoutableListener break; case LicenseSeat::class: $notificationClass = CheckoutLicenseSeatNotification::class; - break; + break; } + return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note); } diff --git a/app/Models/AssetModel.php b/app/Models/AssetModel.php index d0e47e1cf1..c5fb9284aa 100755 --- a/app/Models/AssetModel.php +++ b/app/Models/AssetModel.php @@ -46,15 +46,6 @@ class AssetModel extends SnipeModel protected $injectUniqueIdentifier = true; use ValidatingTrait; - public function setEolAttribute($value) - { - if ($value == '') { - $value = 0; - } - - $this->attributes['eol'] = $value; - } - /** * The attributes that are mass assignable. * diff --git a/app/Models/SamlNonce.php b/app/Models/SamlNonce.php new file mode 100644 index 0000000000..6eb05352d8 --- /dev/null +++ b/app/Models/SamlNonce.php @@ -0,0 +1,15 @@ +webhook_endpoint != '') { + if (Setting::getSettings()->webhook_selected == 'microsoft'){ + + $notifyBy[] = MicrosoftTeamsChannel::class; + } + + if (Setting::getSettings()->webhook_selected == 'slack') { $notifyBy[] = 'slack'; } @@ -108,6 +114,24 @@ class CheckinAccessoryNotification extends Notification ->content($note); }); } + public function toMicrosoftTeams() + { + $admin = $this->admin; + $item = $this->item; + $note = $this->note; + + return MicrosoftTeamsMessage::create() + ->to($this->settings->webhook_endpoint) + ->type('success') + ->addStartGroupToSection('activityTitle') + ->title(trans('Accessory_Checkin_Notification')) + ->addStartGroupToSection('activityText') + ->fact(htmlspecialchars_decode($item->present()->name), '', 'activityTitle') + ->fact(trans('mail.checked_into'), $item->location->name ? $item->location->name : '') + ->fact(trans('mail.Accessory_Checkin_Notification')." by ", $admin->present()->fullName()) + ->fact(trans('admin/consumables/general.remaining'), $item->numRemaining()) + ->fact(trans('mail.notes'), $note ?: ''); + } /** * Get the mail representation of the notification. diff --git a/app/Notifications/CheckinAssetNotification.php b/app/Notifications/CheckinAssetNotification.php index 05e56a9619..8e9467c54f 100644 --- a/app/Notifications/CheckinAssetNotification.php +++ b/app/Notifications/CheckinAssetNotification.php @@ -10,6 +10,8 @@ use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; class CheckinAssetNotification extends Notification { @@ -44,7 +46,12 @@ class CheckinAssetNotification extends Notification public function via() { $notifyBy = []; - if (Setting::getSettings()->webhook_endpoint != '') { + + if (Setting::getSettings()->webhook_selected == 'microsoft'){ + + $notifyBy[] = MicrosoftTeamsChannel::class; + } + if (Setting::getSettings()->webhook_selected == 'slack') { \Log::debug('use webhook'); $notifyBy[] = 'slack'; } @@ -84,6 +91,23 @@ class CheckinAssetNotification extends Notification ->content($note); }); } + public function toMicrosoftTeams() + { + $admin = $this->admin; + $item = $this->item; + $note = $this->note; + + return MicrosoftTeamsMessage::create() + ->to($this->settings->webhook_endpoint) + ->type('success') + ->title(trans('mail.Asset_Checkin_Notification')) + ->addStartGroupToSection('activityText') + ->fact(htmlspecialchars_decode($item->present()->name), '', 'activityText') + ->fact(trans('mail.checked_into'), $item->location->name ? $item->location->name : '') + ->fact(trans('mail.Asset_Checkin_Notification')." by ", $admin->present()->fullName()) + ->fact(trans('admin/hardware/form.status'), $item->assetstatus->name) + ->fact(trans('mail.notes'), $note ?: ''); + } /** * Get the mail representation of the notification. diff --git a/app/Notifications/CheckinLicenseSeatNotification.php b/app/Notifications/CheckinLicenseSeatNotification.php index 2c7fe2fd85..2222f937f3 100644 --- a/app/Notifications/CheckinLicenseSeatNotification.php +++ b/app/Notifications/CheckinLicenseSeatNotification.php @@ -9,6 +9,8 @@ use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; class CheckinLicenseSeatNotification extends Notification { @@ -41,7 +43,12 @@ class CheckinLicenseSeatNotification extends Notification { $notifyBy = []; - if (Setting::getSettings()->webhook_endpoint != '') { + if (Setting::getSettings()->webhook_selected == 'microsoft'){ + + $notifyBy[] = MicrosoftTeamsChannel::class; + } + + if (Setting::getSettings()->webhook_selected == 'slack') { $notifyBy[] = 'slack'; } @@ -87,6 +94,25 @@ class CheckinLicenseSeatNotification extends Notification ->content($note); }); } + public function toMicrosoftTeams() + { + $target = $this->target; + $admin = $this->admin; + $item = $this->item; + $note = $this->note; + + return MicrosoftTeamsMessage::create() + ->to($this->settings->webhook_endpoint) + ->type('success') + ->addStartGroupToSection('activityTitle') + ->title(trans('mail.License_Checkin_Notification')) + ->addStartGroupToSection('activityText') + ->fact(htmlspecialchars_decode($item->present()->name), '', 'header') + ->fact(trans('mail.License_Checkin_Notification')." by ", $admin->present()->fullName() ?: 'CLI tool') + ->fact(trans('mail.checkedin_from'), $target->present()->fullName()) + ->fact(trans('admin/consumables/general.remaining'), $item->availCount()->count()) + ->fact(trans('mail.notes'), $note ?: ''); + } /** * Get the mail representation of the notification. diff --git a/app/Notifications/CheckoutAccessoryNotification.php b/app/Notifications/CheckoutAccessoryNotification.php index f5635d1af0..62fce8e176 100644 --- a/app/Notifications/CheckoutAccessoryNotification.php +++ b/app/Notifications/CheckoutAccessoryNotification.php @@ -9,6 +9,8 @@ use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; class CheckoutAccessoryNotification extends Notification { @@ -24,7 +26,6 @@ class CheckoutAccessoryNotification extends Notification $this->note = $note; $this->target = $checkedOutTo; $this->acceptance = $acceptance; - $this->settings = Setting::getSettings(); } @@ -37,7 +38,12 @@ class CheckoutAccessoryNotification extends Notification { $notifyBy = []; - if (Setting::getSettings()->webhook_endpoint != '') { + if (Setting::getSettings()->webhook_selected == 'microsoft'){ + + $notifyBy[] = MicrosoftTeamsChannel::class; + } + + if (Setting::getSettings()->webhook_selected == 'slack') { $notifyBy[] = 'slack'; } @@ -96,6 +102,27 @@ class CheckoutAccessoryNotification extends Notification ->content($note); }); } + public function toMicrosoftTeams() + { + $target = $this->target; + $admin = $this->admin; + $item = $this->item; + $note = $this->note; + + return MicrosoftTeamsMessage::create() + ->to($this->settings->webhook_endpoint) + ->type('success') + ->addStartGroupToSection('activityTitle') + ->title(trans('mail.Accessory_Checkout_Notification')) + ->addStartGroupToSection('activityText') + ->fact(htmlspecialchars_decode($item->present()->name), '', 'activityTitle') + ->fact(trans('mail.assigned_to'), $target->present()->name) + ->fact(trans('mail.checkedout_from'), $item->location->name ? $item->location->name : '') + ->fact(trans('mail.Accessory_Checkout_Notification') . " by ", $admin->present()->fullName()) + ->fact(trans('admin/consumables/general.remaining'), $item->numRemaining()) + ->fact(trans('mail.notes'), $note ?: ''); + + } /** * Get the mail representation of the notification. diff --git a/app/Notifications/CheckoutAssetNotification.php b/app/Notifications/CheckoutAssetNotification.php index e57825f5c6..7350a7736b 100644 --- a/app/Notifications/CheckoutAssetNotification.php +++ b/app/Notifications/CheckoutAssetNotification.php @@ -6,10 +6,13 @@ use App\Helpers\Helper; use App\Models\Asset; use App\Models\Setting; use App\Models\User; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; class CheckoutAssetNotification extends Notification { @@ -51,9 +54,13 @@ class CheckoutAssetNotification extends Notification */ public function via() { + if (Setting::getSettings()->webhook_selected == 'microsoft'){ + + return [MicrosoftTeamsChannel::class]; + } $notifyBy = []; - if ((Setting::getSettings()) && (Setting::getSettings()->webhook_endpoint != '')) { + if ((Setting::getSettings()) && (Setting::getSettings()->webhook_selected == 'slack')) { \Log::debug('use webhook'); $notifyBy[] = 'slack'; } @@ -117,6 +124,25 @@ class CheckoutAssetNotification extends Notification ->content($note); }); } + public function toMicrosoftTeams() + { + $target = $this->target; + $admin = $this->admin; + $item = $this->item; + $note = $this->note; + + return MicrosoftTeamsMessage::create() + ->to($this->settings->webhook_endpoint) + ->type('success') + ->title(trans('mail.Asset_Checkout_Notification')) + ->addStartGroupToSection('activityText') + ->fact(trans('mail.assigned_to'), $target->present()->name) + ->fact(htmlspecialchars_decode($item->present()->name), '', 'activityText') + ->fact(trans('mail.Asset_Checkout_Notification') . " by ", $admin->present()->fullName()) + ->fact(trans('mail.notes'), $note ?: ''); + + + } /** * Get the mail representation of the notification. diff --git a/app/Notifications/CheckoutConsumableNotification.php b/app/Notifications/CheckoutConsumableNotification.php index 376c70fdea..228c4c2e95 100644 --- a/app/Notifications/CheckoutConsumableNotification.php +++ b/app/Notifications/CheckoutConsumableNotification.php @@ -9,6 +9,8 @@ use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; class CheckoutConsumableNotification extends Notification { @@ -43,7 +45,12 @@ class CheckoutConsumableNotification extends Notification { $notifyBy = []; - if (Setting::getSettings()->webhook_endpoint != '') { + if (Setting::getSettings()->webhook_selected == 'microsoft'){ + + $notifyBy[] = MicrosoftTeamsChannel::class; + } + + if (Setting::getSettings()->webhook_selected == 'slack') { $notifyBy[] = 'slack'; } @@ -102,6 +109,25 @@ class CheckoutConsumableNotification extends Notification ->content($note); }); } + public function toMicrosoftTeams() + { + $target = $this->target; + $admin = $this->admin; + $item = $this->item; + $note = $this->note; + + return MicrosoftTeamsMessage::create() + ->to($this->settings->webhook_endpoint) + ->type('success') + ->addStartGroupToSection('activityTitle') + ->title(trans('mail.Consumable_checkout_notification')) + ->addStartGroupToSection('activityText') + ->fact(htmlspecialchars_decode($item->present()->name), '', 'activityTitle') + ->fact(trans('mail.Consumable_checkout_notification')." by ", $admin->present()->fullName()) + ->fact(trans('mail.assigned_to'), $target->present()->fullName()) + ->fact(trans('admin/consumables/general.remaining'), $item->numRemaining()) + ->fact(trans('mail.notes'), $note ?: ''); + } /** * Get the mail representation of the notification. diff --git a/app/Notifications/CheckoutLicenseSeatNotification.php b/app/Notifications/CheckoutLicenseSeatNotification.php index 2dd6480a30..d5c9871f3b 100644 --- a/app/Notifications/CheckoutLicenseSeatNotification.php +++ b/app/Notifications/CheckoutLicenseSeatNotification.php @@ -9,6 +9,8 @@ use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; +use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; class CheckoutLicenseSeatNotification extends Notification { @@ -41,9 +43,15 @@ class CheckoutLicenseSeatNotification extends Notification */ public function via() { + $notifyBy = []; - if (Setting::getSettings()->webhook_endpoint != '') { + if (Setting::getSettings()->webhook_selected == 'microsoft'){ + + $notifyBy[] = MicrosoftTeamsChannel::class; + } + + if (Setting::getSettings()->webhook_selected == 'slack') { $notifyBy[] = 'slack'; } @@ -102,6 +110,25 @@ class CheckoutLicenseSeatNotification extends Notification ->content($note); }); } + public function toMicrosoftTeams() + { + $target = $this->target; + $admin = $this->admin; + $item = $this->item; + $note = $this->note; + + return MicrosoftTeamsMessage::create() + ->to($this->settings->webhook_endpoint) + ->type('success') + ->addStartGroupToSection('activityTitle') + ->title(trans('mail.License_Checkout_Notification')) + ->addStartGroupToSection('activityText') + ->fact(htmlspecialchars_decode($item->present()->name), '', 'activityTitle') + ->fact(trans('mail.License_Checkout_Notification')." by ", $admin->present()->fullName()) + ->fact(trans('mail.assigned_to'), $target->present()->fullName()) + ->fact(trans('admin/consumables/general.remaining'), $item->availCount()->count()) + ->fact(trans('mail.notes'), $note ?: ''); + } /** * Get the mail representation of the notification. diff --git a/app/Providers/SnipeTranslationServiceProvider.php b/app/Providers/SnipeTranslationServiceProvider.php new file mode 100644 index 0000000000..f0ff334484 --- /dev/null +++ b/app/Providers/SnipeTranslationServiceProvider.php @@ -0,0 +1,35 @@ +registerLoader(); + + $this->app->singleton('translator', function ($app) { + $loader = $app['translation.loader']; + + // When registering the translator component, we'll need to set the default + // locale as well as the fallback locale. So, we'll grab the application + // configuration so we can easily get both of these values from there. + $locale = $app['config']['app.locale']; + + $trans = new SnipeTranslator($loader, $locale); //the ONLY changed line + + $trans->setFallback($app['config']['app.fallback_locale']); + + return $trans; + }); + } +} diff --git a/app/Services/Saml.php b/app/Services/Saml.php index f80b1c1fb9..b34501366a 100644 --- a/app/Services/Saml.php +++ b/app/Services/Saml.php @@ -394,6 +394,8 @@ class Saml 'nameIdSPNameQualifier' => $auth->getNameIdSPNameQualifier(), 'sessionIndex' => $auth->getSessionIndex(), 'sessionExpiration' => $auth->getSessionExpiration(), + 'nonce' => $auth->getLastAssertionId(), + 'assertionNotOnOrAfter' => $auth->getLastAssertionNotOnOrAfter(), ]; } diff --git a/app/Services/SnipeTranslator.php b/app/Services/SnipeTranslator.php new file mode 100644 index 0000000000..00107ede9e --- /dev/null +++ b/app/Services/SnipeTranslator.php @@ -0,0 +1,42 @@ +get( + $key, $replace, $locale = $this->localeForChoice($locale) + ); + + // If the given "number" is actually an array or countable we will simply count the + // number of elements in an instance. This allows developers to pass an array of + // items without having to count it on their end first which gives bad syntax. + if (is_array($number) || $number instanceof Countable) { + $number = count($number); + } + + $replace['count'] = $number; + + $underscored_locale = str_replace("-","_",$locale); // OUR CHANGE. + return $this->makeReplacements( // BELOW - that $underscored_locale is the *ONLY* modified part + $this->getSelector()->choose($line, $number, $underscored_locale), $replace + ); + } + +} \ No newline at end of file diff --git a/composer.json b/composer.json index 2a456999e9..a378f15900 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "guzzlehttp/guzzle": "^7.0.1", "intervention/image": "^2.5", "javiereguiluz/easyslugger": "^1.0", + "laravel-notification-channels/microsoft-teams": "^1.1", "laravel/framework": "^8.46", "laravel/helpers": "^1.4", "laravel/passport": "^10.1", diff --git a/composer.lock b/composer.lock index 2d5c6c677e..f4baccc092 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f4f3b6b02d044ed3e54cdd509b01c3dc", + "content-hash": "89580c52de91168aac8321460bd428e2", "packages": [ { "name": "alek13/slack", @@ -3188,6 +3188,63 @@ }, "time": "2015-04-12T19:57:10+00:00" }, + { + "name": "laravel-notification-channels/microsoft-teams", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/laravel-notification-channels/microsoft-teams.git", + "reference": "e2df0129ba430666979eb2ad7033455fd0f6b577" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel-notification-channels/microsoft-teams/zipball/e2df0129ba430666979eb2ad7033455fd0f6b577", + "reference": "e2df0129ba430666979eb2ad7033455fd0f6b577", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.3 || ^7.0", + "illuminate/notifications": "~5.5 || ~6.0 || ~7.0 || ^8.0 || ^9.0 || ^10.0", + "illuminate/support": "~5.5 || ~6.0 || ~7.0 || ^8.0 || ^9.0 || ^10.0", + "php": ">=7.2" + }, + "require-dev": { + "mockery/mockery": "^1.2.3", + "phpunit/phpunit": "^8.0|^9.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NotificationChannels\\MicrosoftTeams\\MicrosoftTeamsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NotificationChannels\\MicrosoftTeams\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Madner", + "email": "tobias.madner@gmx.at", + "homepage": "https://www.pinpoll.com", + "role": "Developer" + } + ], + "description": "A Laravel Notification Channel for Microsoft Teams", + "homepage": "https://github.com/laravel-notification-channels/microsoft-teams", + "support": { + "issues": "https://github.com/laravel-notification-channels/microsoft-teams/issues", + "source": "https://github.com/laravel-notification-channels/microsoft-teams/tree/v1.1.4" + }, + "time": "2023-01-25T16:56:40+00:00" + }, { "name": "laravel/framework", "version": "v8.83.22", @@ -16974,5 +17031,5 @@ "ext-pdo": "*" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/config/app.php b/config/app.php index 1b4f45e45b..b2f14f3787 100755 --- a/config/app.php +++ b/config/app.php @@ -277,7 +277,8 @@ return [ Illuminate\Redis\RedisServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Session\SessionServiceProvider::class, - Illuminate\Translation\TranslationServiceProvider::class, +// Illuminate\Translation\TranslationServiceProvider::class, //replaced on next line + App\Providers\SnipeTranslationServiceProvider::class, //we REPLACE the default Laravel translator with our own Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, Barryvdh\DomPDF\ServiceProvider::class, diff --git a/config/version.php b/config/version.php index c420df65ba..08dc9f581c 100644 --- a/config/version.php +++ b/config/version.php @@ -1,10 +1,10 @@ 'v6.2.4-pre', - 'full_app_version' => 'v6.2.4-pre - build 12343-gb23ce6cfc', - 'build_version' => '12343', + 'app_version' => 'v6.3.0', + 'full_app_version' => 'v6.3.0 - build 12490-g9136415bb', + 'build_version' => '12490', 'prerelease_version' => '', - 'hash_version' => 'gb23ce6cfc', - 'full_hash' => 'v6.2.4-pre-582-gb23ce6cfc', + 'hash_version' => 'g9136415bb', + 'full_hash' => 'v6.3.0-729-g9136415bb', 'branch' => 'develop', ); \ No newline at end of file diff --git a/database/migrations/2024_01_24_145544_create_saml_nonce_table.php b/database/migrations/2024_01_24_145544_create_saml_nonce_table.php new file mode 100644 index 0000000000..f6305288ee --- /dev/null +++ b/database/migrations/2024_01_24_145544_create_saml_nonce_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('nonce')->index(); + $table->datetime('not_valid_after')->index(); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('saml_nonces'); + } +} diff --git a/resources/lang/en-US/admin/models/message.php b/resources/lang/en-US/admin/models/message.php index 4dbcd4e75e..cc38c54530 100644 --- a/resources/lang/en-US/admin/models/message.php +++ b/resources/lang/en-US/admin/models/message.php @@ -34,7 +34,7 @@ return array( 'bulkedit' => array( 'error' => 'No fields were changed, so nothing was updated.', 'success' => 'Model successfully updated. |:model_count models successfully updated.', - 'warn' => 'You are about to update the properies of the following model: |You are about to edit the properties of the following :model_count models:', + 'warn' => 'You are about to update the properties of the following model:|You are about to edit the properties of the following :model_count models:', ), diff --git a/resources/lang/en-US/admin/settings/general.php b/resources/lang/en-US/admin/settings/general.php index a05b0b6b71..21e5759053 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -204,7 +204,7 @@ return [ 'integrations' => 'Integrations', 'slack' => 'Slack', 'general_webhook' => 'General Webhook', - 'google_workspaces' => 'Google Workspaces', + 'ms_teams' => 'Microsoft Teams', 'webhook' => ':app', 'webhook_presave' => 'Test to Save', 'webhook_title' => 'Update Webhook Settings', diff --git a/resources/lang/en-US/mail.php b/resources/lang/en-US/mail.php index 7dd8d6181c..418de5806f 100644 --- a/resources/lang/en-US/mail.php +++ b/resources/lang/en-US/mail.php @@ -1,10 +1,33 @@ 'A user has accepted an item', - 'acceptance_asset_declined' => 'A user has declined an item', + + 'Accessory_Checkin_Notification' => 'Accessory checked in', + 'Accessory_Checkout_Notification' => 'Accessory checked out', + 'Asset_Checkin_Notification' => 'Asset checked in', + 'Asset_Checkout_Notification' => 'Asset checked out', + 'Confirm_Accessory_Checkin' => 'Accessory checkin confirmation', + 'Confirm_Asset_Checkin' => 'Asset checkin confirmation', + 'Confirm_accessory_delivery' => 'Accessory delivery confirmation', + 'Confirm_asset_delivery' => 'Asset delivery confirmation', + 'Confirm_consumable_delivery' => 'Consumable delivery confirmation', + 'Confirm_license_delivery' => 'License delivery confirmation', + 'Consumable_checkout_notification' => 'Consumable checked out', + 'Days' => 'Days', + 'Expected_Checkin_Date' => 'An asset checked out to you is due to be checked back in on :date', + 'Expected_Checkin_Notification' => 'Reminder: :name checkin deadline approaching', + 'Expected_Checkin_Report' => 'Expected asset checkin report', + 'Expiring_Assets_Report' => 'Expiring Assets Report.', + 'Expiring_Licenses_Report' => 'Expiring Licenses Report.', + 'Item_Request_Canceled' => 'Item Request Canceled', + 'Item_Requested' => 'Item Requested', + 'License_Checkin_Notification' => 'License checked in', + 'License_Checkout_Notification' => 'License checked out', + 'Low_Inventory_Report' => 'Low Inventory Report', 'a_user_canceled' => 'A user has canceled an item request on the website', 'a_user_requested' => 'A user has requested an item on the website', + 'acceptance_asset_accepted' => 'A user has accepted an item', + 'acceptance_asset_declined' => 'A user has declined an item', 'accessory_name' => 'Accessory Name:', 'additional_notes' => 'Additional Notes:', 'admin_has_created' => 'An administrator has created an account for you on the :web website.', @@ -12,59 +35,51 @@ return [ 'asset_name' => 'Asset Name:', 'asset_requested' => 'Asset requested', 'asset_tag' => 'Asset Tag', + 'assets_warrantee_alert' => 'There is :count asset with a warranty expiring in the next :threshold days.|There are :count assets with warranties expiring in the next :threshold days.', 'assigned_to' => 'Assigned To', 'best_regards' => 'Best regards,', 'canceled' => 'Canceled:', 'checkin_date' => 'Checkin Date:', 'checkout_date' => 'Checkout Date:', - 'click_to_confirm' => 'Please click on the following link to confirm your :web account:', + 'checkedout_from' => 'Checked out from', + 'checked_into' => 'Checked into', 'click_on_the_link_accessory' => 'Please click on the link at the bottom to confirm that you have received the accessory.', 'click_on_the_link_asset' => 'Please click on the link at the bottom to confirm that you have received the asset.', - 'Confirm_Asset_Checkin' => 'Asset checkin confirmation', - 'Confirm_Accessory_Checkin' => 'Accessory checkin confirmation', - 'Confirm_accessory_delivery' => 'Accessory delivery confirmation', - 'Confirm_license_delivery' => 'License delivery confirmation', - 'Confirm_asset_delivery' => 'Asset delivery confirmation', - 'Confirm_consumable_delivery' => 'Consumable delivery confirmation', + 'click_to_confirm' => 'Please click on the following link to confirm your :web account:', 'current_QTY' => 'Current QTY', - 'Days' => 'Days', 'days' => 'Days', 'expecting_checkin_date' => 'Expected Checkin Date:', 'expires' => 'Expires', - 'Expiring_Assets_Report' => 'Expiring Assets Report.', - 'Expiring_Licenses_Report' => 'Expiring Licenses Report.', 'hello' => 'Hello', 'hi' => 'Hi', 'i_have_read' => 'I have read and agree to the terms of use, and have received this item.', - 'item' => 'Item:', - 'Item_Request_Canceled' => 'Item Request Canceled', - 'Item_Requested' => 'Item Requested', - 'link_to_update_password' => 'Please click on the following link to update your :web password:', - 'login_first_admin' => 'Login to your new Snipe-IT installation using the credentials below:', - 'login' => 'Login:', - 'Low_Inventory_Report' => 'Low Inventory Report', 'inventory_report' => 'Inventory Report', + 'item' => 'Item:', + 'license_expiring_alert' => 'There is :count license expiring in the next :threshold days.|There are :count licenses expiring in the next :threshold days.', + 'link_to_update_password' => 'Please click on the following link to update your :web password:', + 'login' => 'Login:', + 'login_first_admin' => 'Login to your new Snipe-IT installation using the credentials below:', + 'low_inventory_alert' => 'There is :count item that is below minimum inventory or will soon be low.|There are :count items that are below minimum inventory or will soon be low.', 'min_QTY' => 'Min QTY', 'name' => 'Name', 'new_item_checked' => 'A new item has been checked out under your name, details are below.', + 'notes' => 'Notes', 'password' => 'Password:', 'password_reset' => 'Password Reset', - 'read_the_terms' => 'Please read the terms of use below.', - 'read_the_terms_and_click' => 'Please read the terms of use below, and click on the link at the bottom to confirm that you read - and agree to the terms of use, and have received the asset.', + 'read_the_terms_and_click' => 'Please read the terms of use below, and click on the link at the bottom to confirm that you read and agree to the terms of use, and have received the asset.', 'requested' => 'Requested:', 'reset_link' => 'Your Password Reset Link', 'reset_password' => 'Click here to reset your password:', + 'rights_reserved' => 'All rights reserved.', 'serial' => 'Serial', + 'snipe_webhook_test' => 'Snipe-IT Integration Test', + 'snipe_webhook_summary' => 'Snipe-IT Integration Test Summary', 'supplier' => 'Supplier', 'tag' => 'Tag', 'test_email' => 'Test Email from Snipe-IT', 'test_mail_text' => 'This is a test from the Snipe-IT Asset Management System. If you got this, mail is working :)', 'the_following_item' => 'The following item has been checked in: ', - 'low_inventory_alert' => 'There is :count item that is below minimum inventory or will soon be low.|There are :count items that are below minimum inventory or will soon be low.', - 'assets_warrantee_alert' => 'There is :count asset with a warranty expiring in the next :threshold days.|There are :count assets with warranties expiring in the next :threshold days.', - 'license_expiring_alert' => 'There is :count license expiring in the next :threshold days.|There are :count licenses expiring in the next :threshold days.', 'to_reset' => 'To reset your :web password, complete this form:', 'type' => 'Type', 'upcoming-audits' => 'There is :count asset that is coming up for audit within :threshold days.|There are :count assets that are coming up for audit within :threshold days.', @@ -72,14 +87,6 @@ return [ 'username' => 'Username', 'welcome' => 'Welcome :name', 'welcome_to' => 'Welcome to :web!', - 'your_credentials' => 'Your Snipe-IT credentials', - 'Accessory_Checkin_Notification' => 'Accessory checked in', - 'Asset_Checkin_Notification' => 'Asset checked in', - 'Asset_Checkout_Notification' => 'Asset checked out', - 'License_Checkin_Notification' => 'License checked in', - 'Expected_Checkin_Report' => 'Expected asset checkin report', - 'Expected_Checkin_Notification' => 'Reminder: :name checkin deadline approaching', - 'Expected_Checkin_Date' => 'An asset checked out to you is due to be checked back in on :date', 'your_assets' => 'View Your Assets', - 'rights_reserved' => 'All rights reserved.', + 'your_credentials' => 'Your Snipe-IT credentials', ]; diff --git a/resources/views/livewire/slack-settings-form.blade.php b/resources/views/livewire/slack-settings-form.blade.php index e24b820f86..ad3271bbc7 100644 --- a/resources/views/livewire/slack-settings-form.blade.php +++ b/resources/views/livewire/slack-settings-form.blade.php @@ -61,9 +61,9 @@