2FAuth/app/Services/SettingService.php

263 lines
7.7 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
2022-11-22 15:15:52 +01:00
use App\Exceptions\DbEncryptionException;
2021-12-02 13:15:53 +01:00
use App\Models\Option;
2022-11-22 15:15:52 +01:00
use Exception;
use Illuminate\Support\Collection;
2023-02-25 21:12:10 +01:00
use Illuminate\Support\Facades\Cache;
2022-11-22 15:15:52 +01:00
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
2021-10-15 23:46:21 +02:00
use Illuminate\Support\Facades\Log;
2022-11-22 15:15:52 +01:00
use Throwable;
2021-12-01 13:47:20 +01:00
class SettingService
{
/**
* All settings
2022-11-22 15:15:52 +01:00
*
2022-11-21 11:16:43 +01:00
* @var Collection<string, mixed>
*/
private Collection $settings;
/**
* Cache duration
*/
private int $minutes = 10;
/**
* Name of the cache item where options are persisted
*/
public const CACHE_ITEM_NAME = 'adminOptions';
/**
* Constructor
*/
public function __construct()
{
$this->settings = Cache::remember(self::CACHE_ITEM_NAME, now()->addMinutes($this->minutes), function () {
self::build();
2023-02-25 21:12:10 +01:00
return $this->settings;
});
}
/**
2021-12-01 13:47:20 +01:00
* Get a setting
*
2024-04-20 19:03:44 +02:00
* @param string $setting A single setting name
2021-12-01 13:47:20 +01:00
* @return mixed string|int|boolean|null
*/
public function get($setting)
{
return $this->settings->get($setting);
}
/**
2021-12-01 13:47:20 +01:00
* Get all settings
2022-11-22 15:15:52 +01:00
*
2022-11-21 11:16:43 +01:00
* @return Collection<string, mixed> the Settings collection
*/
public function all() : Collection
{
return $this->settings;
}
/**
2021-12-01 13:47:20 +01:00
* Set a setting
*
2024-04-20 19:03:44 +02:00
* @param string|array $setting A single setting name or an associative array of name:value settings
* @param string|int|bool|null $value The value for single setting
*/
public function set($setting, $value = null) : void
{
$settings = is_array($setting) ? $setting : [$setting => $value];
foreach ($settings as $setting => $value) {
2022-11-22 15:15:52 +01:00
if ($setting === 'useEncryption') {
2021-11-26 11:21:57 +01:00
$this->setEncryptionTo($value);
}
$settings[$setting] = $this->replaceBoolean($value);
}
2021-10-15 23:46:21 +02:00
foreach ($settings as $setting => $value) {
2021-12-01 13:47:20 +01:00
Option::updateOrCreate(['key' => $setting], ['value' => $value]);
Log::notice(sprintf('App setting %s set to %s', var_export($setting, true), var_export($this->restoreType($value), true)));
2021-10-15 23:46:21 +02:00
}
self::buildAndCache();
}
/**
2021-12-01 13:47:20 +01:00
* Delete a setting
*
2024-04-20 19:03:44 +02:00
* @param string $name The setting name
*/
public function delete(string $name) : void
{
2021-12-01 13:47:20 +01:00
Option::where('key', $name)->delete();
Log::notice(sprintf('App setting %s reset to default', var_export($name, true)));
self::buildAndCache();
}
/**
* Determine if the given setting has been edited
*
* @param string $key
*/
public function isEdited($key) : bool
{
return DB::table('options')->where('key', $key)->exists();
}
/**
* Set the settings collection
2022-11-22 15:15:52 +01:00
*
* @return void
*/
private function build()
{
// Get a collection of saved options
$options = DB::table('options')->pluck('value', 'key');
$options->transform(function ($item, $key) {
return $this->restoreType($item);
});
// Merge customized values with app default values
2023-02-25 21:12:10 +01:00
$settings = collect(config('2fauth.settings'))->merge($options); /** @phpstan-ignore-line */
$this->settings = $settings;
}
/**
* Build and cache the options collection
2023-02-25 21:12:10 +01:00
*
* @return void
*/
private function buildAndCache()
{
self::build();
Cache::put(self::CACHE_ITEM_NAME, $this->settings, now()->addMinutes($this->minutes));
}
/**
* Replaces boolean by a patterned string as appstrack/laravel-options package does not support var type
2022-11-22 15:15:52 +01:00
*
* @return string
*/
private function replaceBoolean(mixed $value)
{
return is_bool($value) ? '{{' . $value . '}}' : $value;
}
/**
* Replaces patterned string that represent booleans with real booleans
2022-11-22 15:15:52 +01:00
*
* @return mixed
*/
private function restoreType(mixed $value)
{
if (is_numeric($value)) {
$value = is_float($value + 0) ? (float) $value : (int) $value;
}
2022-11-22 15:15:52 +01:00
if ($value === '{{}}') {
return false;
2022-11-22 15:15:52 +01:00
} elseif ($value === '{{1}}') {
return true;
2022-11-22 15:15:52 +01:00
} else {
return $value;
}
}
2021-11-26 11:21:57 +01:00
/**
* Enable or Disable encryption of 2FAccounts sensible data
2022-11-22 15:15:52 +01:00
*
*
2021-11-26 11:21:57 +01:00
* @throws DbEncryptionException Something failed, everything have been rolled back
*/
private function setEncryptionTo(bool $state) : void
{
// We don't want the records to be encrypted/decrypted multiple successive times
$isInUse = $this->get('useEncryption');
2022-11-22 15:15:52 +01:00
if ($isInUse === ! $state) {
2021-11-26 11:21:57 +01:00
if ($this->updateRecords($state)) {
if ($state) {
Log::notice('Sensible data are now encrypted');
2022-11-22 15:15:52 +01:00
} else {
Log::notice('Sensible data are now decrypted');
2021-11-26 11:21:57 +01:00
}
2022-11-22 15:15:52 +01:00
} else {
2021-11-26 11:21:57 +01:00
Log::warning('Some data cannot be encrypted/decrypted, the useEncryption setting remain unchanged');
throw new DbEncryptionException($state === true ? __('errors.error_during_encryption') : __('errors.error_during_decryption'));
}
}
}
/**
* Encrypt/Decrypt accounts in database
2022-11-22 15:15:52 +01:00
*
2024-04-20 19:03:44 +02:00
* @param bool $encrypted Whether the record should be encrypted or not
2022-11-22 15:15:52 +01:00
* @return bool Whether the operation completed successfully
2021-11-26 11:21:57 +01:00
*/
private function updateRecords(bool $encrypted) : bool
2022-11-22 15:15:52 +01:00
{
$success = true;
2021-11-26 11:21:57 +01:00
$twofaccounts = DB::table('twofaccounts')->get();
2022-11-22 15:15:52 +01:00
$twofaccounts->each(function ($item, $key) use (&$success, $encrypted) {
2021-11-26 11:21:57 +01:00
try {
// encrypting a null value generate a hash which once decrypted gives an empty string.
// As Service is nullable, we handle it only if the fiel contains a value
if ($item->service) {
$item->service = $encrypted ? Crypt::encryptString($item->service) : Crypt::decryptString($item->service);
}
2022-11-22 15:15:52 +01:00
$item->legacy_uri = $encrypted ? Crypt::encryptString($item->legacy_uri) : Crypt::decryptString($item->legacy_uri);
$item->account = $encrypted ? Crypt::encryptString($item->account) : Crypt::decryptString($item->account);
$item->secret = $encrypted ? Crypt::encryptString($item->secret) : Crypt::decryptString($item->secret);
} catch (Exception $ex) {
2021-11-26 11:21:57 +01:00
$success = false;
2023-12-20 16:55:58 +01:00
2021-11-26 11:21:57 +01:00
// Exit the each iteration
return false;
}
});
if ($success) {
// The whole collection has now its sensible data encrypted/decrypted
// We update the db using a transaction that can rollback everything if an error occured
DB::beginTransaction();
try {
$twofaccounts->each(function ($item, $key) {
DB::table('twofaccounts')
->where('id', $item->id)
->update([
'service' => $item->service,
2021-11-26 11:21:57 +01:00
'legacy_uri' => $item->legacy_uri,
'account' => $item->account,
2022-11-22 15:15:52 +01:00
'secret' => $item->secret,
2021-11-26 11:21:57 +01:00
]);
});
DB::commit();
2022-11-22 15:15:52 +01:00
2021-11-26 11:21:57 +01:00
return true;
}
// @codeCoverageIgnoreStart
catch (Throwable $ex) {
DB::rollBack();
2022-11-22 15:15:52 +01:00
2021-11-26 11:21:57 +01:00
return false;
}
// @codeCoverageIgnoreEnd
2022-11-22 15:15:52 +01:00
} else {
return false;
2021-11-26 11:21:57 +01:00
}
}
2022-11-22 15:15:52 +01:00
}