This commit is contained in:
2024-04-19 10:27:36 +02:00
parent fcb6bbe566
commit 35c96e715c
7852 changed files with 4815 additions and 8 deletions

View File

@ -0,0 +1,195 @@
<?php
namespace App\Services;
use App\Events\CheckoutableCheckedOut;
use App\Models\PredefinedKit;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
/**
* Class incapsulates checkout logic for reuse in different controllers
* @author [D. Minaev.] [<dmitriy.minaev.v@gmail.com>]
*/
class PredefinedKitCheckoutService
{
use AuthorizesRequests;
/**
* @param Request $request, this function works with fields: checkout_at, expected_checkin, note
* @param PredefinedKit $kit kit for checkout
* @param User $user checkout target
* @return array Empty array if all ok, else [string_error1, string_error2...]
*/
public function checkout(Request $request, PredefinedKit $kit, User $user)
{
try {
// Check if the user exists
if (is_null($user)) {
return ['errors' => trans('admin/users/message.user_not_found')];
}
$errors = [];
$assets_to_add = $this->getAssetsToAdd($kit, $user, $errors);
$license_seats_to_add = $this->getLicenseSeatsToAdd($kit, $errors);
$consumables_to_add = $this->getConsumablesToAdd($kit, $errors);
$accessories_to_add = $this->getAccessoriesToAdd($kit, $errors);
if (count($errors) > 0) {
return ['errors' => $errors];
}
$checkout_at = date('Y-m-d H:i:s');
if (($request->filled('checkout_at')) && ($request->get('checkout_at') != date('Y-m-d'))) {
$checkout_at = $request->get('checkout_at');
}
$expected_checkin = '';
if ($request->filled('expected_checkin')) {
$expected_checkin = $request->get('expected_checkin');
}
$admin = Auth::user();
$note = e($request->get('note'));
$errors = $this->saveToDb($user, $admin, $checkout_at, $expected_checkin, $errors, $assets_to_add, $license_seats_to_add, $consumables_to_add, $accessories_to_add, $note);
return ['errors' => $errors, 'assets' => $assets_to_add, 'accessories' => $accessories_to_add, 'consumables' => $consumables_to_add];
} catch (ModelNotFoundException $e) {
return ['errors' => [$e->getMessage()]];
} catch (CheckoutNotAllowed $e) {
return ['errors' => [$e->getMessage()]];
}
}
protected function getAssetsToAdd($kit, $user, &$errors)
{
$models = $kit->models()
->with(['assets' => function ($hasMany) {
$hasMany->RTD();
}])
->get();
$assets_to_add = [];
foreach ($models as $model) {
$assets = $model->assets;
$quantity = $model->pivot->quantity;
foreach ($assets as $asset) {
if (
$asset->availableForCheckout()
&& ! $asset->is($user)
) {
$this->authorize('checkout', $asset);
$quantity -= 1;
$assets_to_add[] = $asset;
if ($quantity <= 0) {
break;
}
}
}
if ($quantity > 0) {
$errors[] = trans('admin/kits/general.none_models', ['model'=> $model->name, 'qty' => $model->pivot->quantity]);
}
}
return $assets_to_add;
}
protected function getLicenseSeatsToAdd($kit, &$errors)
{
$seats_to_add = [];
$licenses = $kit->licenses()
->with('freeSeats')
->get();
foreach ($licenses as $license) {
$quantity = $license->pivot->quantity;
if ($quantity > count($license->freeSeats)) {
$errors[] = trans('admin/kits/general.none_licenses', ['license'=> $license->name, 'qty' => $license->pivot->quantity]);
}
for ($i = 0; $i < $quantity; $i++) {
$seats_to_add[] = $license->freeSeats[$i];
}
}
return $seats_to_add;
}
protected function getConsumablesToAdd($kit, &$errors)
{
$consumables = $kit->consumables()->with('users')->get();
foreach ($consumables as $consumable) {
if ($consumable->numRemaining() < $consumable->pivot->quantity) {
$errors[] = trans('admin/kits/general.none_consumables', ['consumable'=> $consumable->name, 'qty' => $consumable->pivot->quantity]);
}
}
return $consumables;
}
protected function getAccessoriesToAdd($kit, &$errors)
{
$accessories = $kit->accessories()->with('users')->get();
foreach ($accessories as $accessory) {
if ($accessory->numRemaining() < $accessory->pivot->quantity) {
$errors[] = trans('admin/kits/general.none_accessory', ['accessory'=> $accessory->name, 'qty' => $accessory->pivot->quantity]);
}
}
return $accessories;
}
protected function saveToDb($user, $admin, $checkout_at, $expected_checkin, $errors, $assets_to_add, $license_seats_to_add, $consumables_to_add, $accessories_to_add, $note)
{
$errors = DB::transaction(
function () use ($user, $admin, $checkout_at, $expected_checkin, $errors, $assets_to_add, $license_seats_to_add, $consumables_to_add, $accessories_to_add, $note) {
// assets
foreach ($assets_to_add as $asset) {
$asset->location_id = $user->location_id;
$error = $asset->checkOut($user, $admin, $checkout_at, $expected_checkin, $note, null);
if ($error) {
array_merge_recursive($errors, $asset->getErrors()->toArray());
}
}
// licenses
foreach ($license_seats_to_add as $licenseSeat) {
$licenseSeat->user_id = $admin->id;
$licenseSeat->assigned_to = $user->id;
if ($licenseSeat->save()) {
event(new CheckoutableCheckedOut($licenseSeat, $user, $admin, $note));
} else {
$errors[] = 'Something went wrong saving a license seat';
}
}
// consumables
foreach ($consumables_to_add as $consumable) {
$consumable->assigned_to = $user->id;
$consumable->users()->attach($consumable->id, [
'consumable_id' => $consumable->id,
'user_id' => $admin->id,
'assigned_to' => $user->id,
]);
event(new CheckoutableCheckedOut($consumable, $user, $admin, $note));
}
//accessories
foreach ($accessories_to_add as $accessory) {
$accessory->assigned_to = $user->id;
$accessory->users()->attach($accessory->id, [
'accessory_id' => $accessory->id,
'user_id' => $admin->id,
'assigned_to' => $user->id,
]);
event(new CheckoutableCheckedOut($accessory, $user, $admin, $note));
}
return $errors;
}
);
return $errors;
}
}

View File

@ -0,0 +1,516 @@
<?php
namespace App\Services;
use App\Models\Setting;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Session;
use OneLogin\Saml2\Auth as OneLogin_Saml2_Auth;
use OneLogin\Saml2\IdPMetadataParser as OneLogin_Saml2_IdPMetadataParser;
use OneLogin\Saml2\Settings as OneLogin_Saml2_Settings;
use OneLogin\Saml2\Utils as OneLogin_Saml2_Utils;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* SAML Singleton that builds the settings and loads the onelogin/php-saml library.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*/
class Saml
{
public const DATA_SESSION_KEY = '_samlData';
/**
* OneLogin_Saml2_Auth instance.
*
* @var OneLogin\Saml2\Auth
*/
private $_auth;
/**
* if SAML is enabled and has valid settings.
*
* @var bool
*/
private $_enabled = false;
/**
* Settings to be passed to OneLogin_Saml2_Auth.
*
* @var array
*/
private $_settings = [];
/**
* User attributes data.
*
* @var array
*/
private $_attributes = [];
/**
* User attributes data with FriendlyName index.
*
* @var array
*/
private $_attributesWithFriendlyName = [];
/**
* NameID
*
* @var string
*/
private $_nameid;
/**
* NameID Format
*
* @var string
*/
private $_nameidFormat;
/**
* NameID NameQualifier
*
* @var string
*/
private $_nameidNameQualifier;
/**
* NameID SP NameQualifier
*
* @var string
*/
private $_nameidSPNameQualifier;
/**
* If user is authenticated.
*
* @var bool
*/
private $_authenticated = false;
/**
* SessionIndex. When the user is logged, this stored it
* from the AuthnStatement of the SAML Response
*
* @var string
*/
private $_sessionIndex;
/**
* SessionNotOnOrAfter. When the user is logged, this stored it
* from the AuthnStatement of the SAML Response
*
* @var int|null
*/
private $_sessionExpiration;
/**
* Initializes the SAML service and builds the OneLogin_Saml2_Auth instance.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @throws Exception
* @throws Error
*/
public function __construct()
{
$this->loadSettings();
if ($this->isEnabled()) {
$this->loadDataFromSession();
} else {
$this->clearData();
}
try {
$this->_auth = new OneLogin_Saml2_Auth($this->_settings);
} catch (Exception $e) {
if ( $this->isEnabled() ) { // $this->loadSettings() initializes this to true if SAML is enabled by settings.
\Log::warning('Trying OneLogin_Saml2_Auth failed. Setting SAML enabled to false. OneLogin_Saml2_Auth error message is: '. $e->getMessage());
}
$this->_enabled = false;
}
}
/**
* Builds settings from Snipe-IT for OneLogin_Saml2_Auth.
*
* @author Johnson Yi <jyi.dev@outlook.com>
* @author Michael Pietsch <skywalker-11@mi-pietsch.de>
*
* @since 5.0.0
*
* @return void
*/
private function loadSettings()
{
$setting = Setting::getSettings();
$settings = [];
$this->_enabled = $setting->saml_enabled == '1';
if ($this->isEnabled()) {
//Let onelogin/php-saml know to use 'X-Forwarded-*' headers if it is from a trusted proxy
OneLogin_Saml2_Utils::setProxyVars(request()->isFromTrustedProxy());
data_set($settings, 'sp.entityId', config('app.url'));
data_set($settings, 'sp.assertionConsumerService.url', route('saml.acs'));
data_set($settings, 'sp.singleLogoutService.url', route('saml.sls'));
data_set($settings, 'sp.x509cert', $setting->saml_sp_x509cert);
data_set($settings, 'sp.privateKey', $setting->saml_sp_privatekey);
if (! empty($setting->saml_sp_x509certNew)) {
data_set($settings, 'sp.x509certNew', $setting->saml_sp_x509certNew);
} else {
data_set($settings, 'sp.x509certNew', '');
}
if (! empty(data_get($settings, 'sp.privateKey'))) {
data_set($settings, 'security.logoutRequestSigned', true);
data_set($settings, 'security.logoutResponseSigned', true);
}
$idpMetadata = $setting->saml_idp_metadata;
if (! empty($idpMetadata)) {
$updatedAt = $setting->updated_at->timestamp;
$metadataCache = Cache::get('saml_idp_metadata_cache');
try {
$url = null;
$metadataInfo = null;
if (empty($metadataCache) || $metadataCache['updated_at'] != $updatedAt) {
if (filter_var($idpMetadata, FILTER_VALIDATE_URL)) {
$url = $idpMetadata;
$metadataInfo = OneLogin_Saml2_IdPMetadataParser::parseRemoteXML($idpMetadata);
} else {
$metadataInfo = OneLogin_Saml2_IdPMetadataParser::parseXML($idpMetadata);
}
Cache::put('saml_idp_metadata_cache', [
'updated_at' => $updatedAt,
'url' => $url,
'metadata_info' => $metadataInfo,
]);
} else {
$metadataInfo = $metadataCache['metadata_info'];
}
$settings = OneLogin_Saml2_IdPMetadataParser::injectIntoSettings($settings, $metadataInfo);
} catch (Exception $e) {
}
}
$custom_settings = preg_split('/\r\n|\r|\n/', $setting->saml_custom_settings);
if ($custom_settings) {
foreach ($custom_settings as $custom_setting) {
$split = explode('=', $custom_setting, 2);
if (count($split) == 2) {
$boolValue = filter_var($split[1], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if (! is_null($boolValue)) {
$split[1] = $boolValue;
}
data_set($settings, $split[0], $split[1]);
}
}
}
$this->_settings = $settings;
}
}
/**
* Load SAML data from Session.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return void
*/
private function loadDataFromSession()
{
$samlData = collect(session(self::DATA_SESSION_KEY));
$this->_authenticated = ! $samlData->isEmpty();
$this->_nameid = $samlData->get('nameId');
$this->_nameidFormat = $samlData->get('nameIdFormat');
$this->_nameidNameQualifier = $samlData->get('nameIdNameQualifier');
$this->_nameidSPNameQualifier = $samlData->get('nameIdSPNameQualifier');
$this->_sessionIndex = $samlData->get('sessionIndex');
$this->_sessionExpiration = $samlData->get('sessionExpiration');
$this->_attributes = $samlData->get('attributes');
$this->_attributesWithFriendlyName = $samlData->get('attributesWithFriendlyName');
}
/**
* Save SAML data to Session.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @param string $data
*
* @return void
*/
private function saveDataToSession($data)
{
return session([self::DATA_SESSION_KEY => $data]);
}
/**
* Check to see if SAML is enabled and has valid settings.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return bool
*/
public function isEnabled()
{
return $this->_enabled;
}
/**
* Clear SAML data from session.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return void
*/
public function clearData()
{
Session::forget(self::DATA_SESSION_KEY);
$this->loadDataFromSession();
}
/**
* Find user from SAML data.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @param string $data
*
* @return \App\Models\User
*/
public function samlLogin($data)
{
$this->saveDataToSession($data);
$this->loadDataFromSession();
$username = $this->getUsername();
return User::where('username', '=', $username)->whereNull('deleted_at')->where('activated', '=', '1')->first();
}
/**
* Returns the OneLogin_Saml2_Auth instance.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return OneLogin\Saml2\Auth
*/
public function getAuth()
{
if (! $this->isEnabled()) {
throw new HttpException(403, 'SAML not enabled.');
}
return $this->_auth;
}
/**
* Get a setting.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @param string|array|int $key
* @param mixed $default
*
* @return void
*/
public function getSetting($key, $default = null)
{
return data_get($this->_settings, $key, $default);
}
/**
* Gets the SP metadata. The XML representation.
*
* @param bool $alwaysPublishEncryptionCert When 'true', the returned
* metadata will always include an 'encryption' KeyDescriptor. Otherwise,
* the 'encryption' KeyDescriptor will only be included if
* $advancedSettings['security']['wantNameIdEncrypted'] or
* $advancedSettings['security']['wantAssertionsEncrypted'] are enabled.
* @param int|null $validUntil Metadata's valid time
* @param int|null $cacheDuration Duration of the cache in seconds
*
* @return string SP metadata (xml)
*/
public function getSPMetadata($alwaysPublishEncryptionCert = false, $validUntil = null, $cacheDuration = null)
{
try {
$settings = new OneLogin_Saml2_Settings($this->_settings, true);
$metadata = $settings->getSPMetadata($alwaysPublishEncryptionCert, $validUntil, $cacheDuration);
return $metadata;
} catch (Exception $e) {
return '';
}
}
/**
* Extract data from SAML Response.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return array
*/
public function extractData()
{
$auth = $this->getAuth();
return [
'attributes' => $auth->getAttributes(),
'attributesWithFriendlyName' => $auth->getAttributesWithFriendlyName(),
'nameId' => $auth->getNameId(),
'nameIdFormat' => $auth->getNameIdFormat(),
'nameIdNameQualifier' => $auth->getNameIdNameQualifier(),
'nameIdSPNameQualifier' => $auth->getNameIdSPNameQualifier(),
'sessionIndex' => $auth->getSessionIndex(),
'sessionExpiration' => $auth->getSessionExpiration(),
'nonce' => $auth->getLastAssertionId(),
'assertionNotOnOrAfter' => $auth->getLastAssertionNotOnOrAfter(),
];
}
/**
* Checks if the user is authenticated or not.
*
* @return bool True if the user is authenticated
*/
public function isAuthenticated()
{
return $this->_authenticated;
}
/**
* Returns the set of SAML attributes.
*
* @return array Attributes of the user.
*/
public function getAttributes()
{
return $this->_attributes;
}
/**
* Returns the set of SAML attributes indexed by FriendlyName
*
* @return array Attributes of the user.
*/
public function getAttributesWithFriendlyName()
{
return $this->_attributesWithFriendlyName;
}
/**
* Returns the nameID
*
* @return string The nameID of the assertion
*/
public function getNameId()
{
return $this->_nameid;
}
/**
* Returns the nameID Format
*
* @return string The nameID Format of the assertion
*/
public function getNameIdFormat()
{
return $this->_nameidFormat;
}
/**
* Returns the nameID NameQualifier
*
* @return string The nameID NameQualifier of the assertion
*/
public function getNameIdNameQualifier()
{
return $this->_nameidNameQualifier;
}
/**
* Returns the nameID SP NameQualifier
*
* @return string The nameID SP NameQualifier of the assertion
*/
public function getNameIdSPNameQualifier()
{
return $this->_nameidSPNameQualifier;
}
/**
* Returns the SessionIndex
*
* @return string|null The SessionIndex of the assertion
*/
public function getSessionIndex()
{
return $this->_sessionIndex;
}
/**
* Returns the SessionNotOnOrAfter
*
* @return int|null The SessionNotOnOrAfter of the assertion
*/
public function getSessionExpiration()
{
return $this->_sessionExpiration;
}
/**
* Returns the correct username from SAML Response.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return string
*/
public function getUsername()
{
$setting = Setting::getSettings();
$username = $this->getNameId();
if (! empty($setting->saml_attr_mapping_username)) {
$attributes = $this->getAttributes();
if (isset($attributes[$setting->saml_attr_mapping_username])) {
$username = $attributes[$setting->saml_attr_mapping_username][0];
}
}
return $username;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Services;
use Illuminate\Translation\Translator;
/***************************************************************
* This is just a very, very light modification to the default Laravel Translator.
* The only difference it has is that it modifies the $locale
* value when the pluralizations are done so we can switch over from en-US to en_US (for example).
*
* This means our translation directories can stay where they are (en-US), but the
* pluralization code can get executed against a locale of en_US
* (Which is required by Symfony, for some inexplicable reason)
*
* This method is called by the trans_choice() helper, which we *do* use a lot.
***************************************************************/
class SnipeTranslator extends Translator {
//This is copied-and-pasted (almost) verbatim from Illuminate\Translation\Translator
public function choice($key, $number, array $replace = [], $locale = null)
{
$line = $this->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
);
}
}