Page MenuHomePhorge

D5472.1775321984.diff
No OneTemporary

Authored By
Unknown
Size
25 KB
Referenced Files
None
Subscribers
None

D5472.1775321984.diff

diff --git a/src/app/Backends/Roundcube.php b/src/app/Backends/Roundcube.php
--- a/src/app/Backends/Roundcube.php
+++ b/src/app/Backends/Roundcube.php
@@ -5,6 +5,7 @@
use App\User;
use App\UserAlias;
use Illuminate\Database\ConnectionInterface;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
@@ -293,6 +294,24 @@
return true;
}
+ /**
+ * Reset the cache entry for webmail configuration passed to the Kolab plugin.
+ *
+ * @param User $user User
+ */
+ public static function resetConfigCache(User $user): void
+ {
+ $user_id = self::userId($user->email, false);
+
+ if (!$user_id) {
+ return;
+ }
+
+ $cache_key = "{$user_id}:kolab_client:get:api/v4/config/webmail";
+
+ Cache::store('roundcube')->forget($cache_key);
+ }
+
/**
* Validate user identities, remove those that user no longer has access to.
*
diff --git a/src/app/Console/Commands/User/DebugStartCommand.php b/src/app/Console/Commands/User/DebugStartCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/User/DebugStartCommand.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console\Commands\User;
+
+use App\Console\Command;
+
+class DebugStartCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:debug-start {user} {mode?}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Enable debug for user.';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = $this->getUser($this->argument('user'));
+
+ if (!$user) {
+ $this->error("User not found.");
+ return 1;
+ }
+
+ $mode = \strtolower($this->argument('mode') ?: 'roundcube,syncroton,chwala');
+
+ $user->setSetting('debug', $mode);
+ }
+}
diff --git a/src/app/Console/Commands/User/DebugStopCommand.php b/src/app/Console/Commands/User/DebugStopCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/User/DebugStopCommand.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Console\Commands\User;
+
+use App\Console\Command;
+
+class DebugStopCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:debug-stop {user}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Disable debug for user.';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = $this->getUser($this->argument('user'));
+
+ if (!$user) {
+ $this->error("User not found.");
+ return 1;
+ }
+
+ $user->removeSetting('debug');
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -411,16 +411,20 @@
return $this->errorResponse(403);
}
- // For now admins can change only user external email address
+ // For now admins can change only user external email address and debug mode
$rules = [];
+ $input = $request->input();
- if (array_key_exists('external_email', $request->input())) {
+ if (array_key_exists('external_email', $input)) {
$rules['external_email'] = 'email';
}
+ if (array_key_exists('debug', $input) && $this->guard()->user()->role == User::ROLE_ADMIN) {
+ $rules['debug'] = 'nullable|string|max:255';
+ }
// Validate input
- $v = Validator::make($request->all(), $rules);
+ $v = Validator::make($input, $rules);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
diff --git a/src/app/Http/Controllers/API/V4/ConfigController.php b/src/app/Http/Controllers/API/V4/ConfigController.php
--- a/src/app/Http/Controllers/API/V4/ConfigController.php
+++ b/src/app/Http/Controllers/API/V4/ConfigController.php
@@ -3,11 +3,14 @@
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
+use App\UserSetting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ConfigController extends Controller
{
+ public const DEBUG_TTL = 12; // hours
+
/**
* Get the per-user webmail configuration.
*
@@ -42,6 +45,16 @@
$config['kolab-configuration-overlays'][] = 'groupware';
}
+ if ($debug_setting = $user->settings()->where('key', 'debug')->first()) {
+ /** @var UserSetting $debug_setting */
+ // Make sure the setting didn't expire
+ if ($debug_setting->updated_at->isBefore(now()->subHours(self::DEBUG_TTL))) {
+ $debug_setting->delete();
+ } else {
+ $config['debug'] = $debug_setting->value;
+ }
+ }
+
// TODO: Per-domain configuration, e.g. skin/logo
// $config['skin'] = 'apostrophy';
// $config['skin_logo'] = 'data:image/svg+xml;base64,'
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -459,7 +459,7 @@
$response['isLocked'] = !$user->isActive() && $wallet->plan()?->mode == Plan::MODE_MANDATE;
// Settings
- $keys = array_merge(self::USER_SETTINGS, ['password_expired']);
+ $keys = array_merge(self::USER_SETTINGS, ['password_expired', 'debug']);
$response['settings'] = $user->settings()->whereIn('key', $keys)->pluck('value', 'key')->all();
// Status info
diff --git a/src/app/Observers/UserSettingObserver.php b/src/app/Observers/UserSettingObserver.php
--- a/src/app/Observers/UserSettingObserver.php
+++ b/src/app/Observers/UserSettingObserver.php
@@ -3,6 +3,7 @@
namespace App\Observers;
use App\Jobs\User\UpdateJob;
+use App\Support\Facades\Roundcube;
use App\UserSetting;
class UserSettingObserver
@@ -47,5 +48,9 @@
if ($userSetting->isBackendSetting()) {
UpdateJob::dispatch($userSetting->user_id);
}
+
+ if ($userSetting->key === 'debug') {
+ Roundcube::resetConfigCache($userSetting->user);
+ }
}
}
diff --git a/src/app/UserSetting.php b/src/app/UserSetting.php
--- a/src/app/UserSetting.php
+++ b/src/app/UserSetting.php
@@ -17,6 +17,12 @@
{
use BelongsToUserTrait;
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'updated_at' => 'datetime:Y-m-d H:i:s',
+ ];
+
/** @var list<string> The attributes that are mass assignable */
protected $fillable = ['user_id', 'key', 'value'];
diff --git a/src/config/cache.php b/src/config/cache.php
--- a/src/config/cache.php
+++ b/src/config/cache.php
@@ -74,6 +74,15 @@
'lock_connection' => 'default',
],
+ 'roundcube' => [
+ 'driver' => 'redis',
+ 'connection' => 'roundcube',
+ 'lock_connection' => null,
+ // Unset the Laravel's cache prefix
+ // Note: It seems it have to be in both config/database.php and here
+ 'prefix' => '',
+ ],
+
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
diff --git a/src/config/database.php b/src/config/database.php
--- a/src/config/database.php
+++ b/src/config/database.php
@@ -144,5 +144,17 @@
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_CACHE_DB', 1),
],
+
+ 'roundcube' => [
+ 'url' => env('REDIS_URL'),
+ 'host' => env('REDIS_HOST', '127.0.0.1'),
+ 'username' => env('REDIS_USERNAME'),
+ 'password' => env('REDIS_PASSWORD'),
+ 'port' => env('REDIS_PORT', 6379),
+ 'database' => env('REDIS_CACHE_DB', 1),
+ // Unset Laravel's cache prefix
+ // FIXME: It seems it have to be in both config/cache.php and here
+ 'prefix' => '',
+ ],
],
];
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -20,6 +20,7 @@
],
'btn' => [
+ 'actions' => "Actions",
'add' => "Add",
'accept' => "Accept",
'back' => "Back",
@@ -481,6 +482,8 @@
'suspended' => "Suspended",
'notready' => "Not Ready",
'active' => "Active",
+ 'on' => "On",
+ 'off' => "Off",
],
'support' => [
@@ -513,6 +516,7 @@
'country' => "Country",
'create' => "Create user",
'custno' => "Customer No.",
+ 'debug-mode' => "Debug mode",
'degraded-warning' => "The account is degraded. Some features have been disabled.",
'degraded-hint' => "Please, make a payment.",
'delegation' => "Delegation",
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -2,7 +2,20 @@
<div class="container">
<div class="card" id="user-info">
<div class="card-body">
- <h1 class="card-title">{{ user.email }}</h1>
+ <h1 class="card-title lh-base">
+ {{ user.email }}
+ <div class="dropdown float-end">
+ <btn class="btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ $t('btn.actions') }}</btn>
+ <ul class="dropdown-menu">
+ <li><btn :id="`button-${suspendAction}`" class="dropdown-item" @click="setSuspendState">{{ $t(`btn.${suspendAction}`) }}</btn></li>
+ <li><btn id="button-resync" class="dropdown-item" @click="resyncUser">{{ $t('btn.resync') }}</btn></li>
+ <li v-if="isAdmin"><btn id="button-debug" class="dropdown-item" @click="$refs.debugDialog.show()">
+ {{ $t('user.debug-mode') }}
+ <span :class="'float-end badge bg-danger rounded-pill' + (user.settings.debug ? '' : ' d-none')" style="top:1px">{{ $t('status.on') }}</span>
+ </btn></li>
+ </ul>
+ </div>
+ </h1>
<div class="card-text">
<form class="read-only short">
<div v-if="user.wallet.user_id != user.id" class="row plaintext">
@@ -77,14 +90,6 @@
</div>
</div>
</form>
- <div class="mt-2 buttons">
- <btn :id="`button-${suspendAction}`" class="btn-outline-primary" @click="setSuspendState">
- {{ $t(`btn.${suspendAction}`) }}
- </btn>
- <btn id="button-resync" class="btn-outline-primary" @click="resyncUser">
- {{ $t('btn.resync') }}
- </btn>
- </div>
</div>
</div>
</div>
@@ -258,6 +263,13 @@
<textarea v-model="comment" name="comment" class="form-control" :placeholder="$t('form.comment')" rows="3"></textarea>
</modal-dialog>
+ <modal-dialog id="debug-dialog" ref="debugDialog" :title="$t('user.debug-mode')" @click="submitDebug()" :buttons="['submit']">
+ <div class="form-check" v-for="mode in debug_modes" :key="mode">
+ <input :id="`debug_${mode}`" :value="mode" type="checkbox" class="form-check-input" :checked="hasDebug(mode)">
+ <label :for="`debug_${mode}`" class="form-check-label">{{ mode }}</label>
+ </div>
+ </modal-dialog>
+
<modal-dialog id="oneoff-dialog" ref="oneoffDialog" @click="submitOneOff()" :buttons="['submit']"
:title="$t(oneoff_negative ? 'user.add-penalty-title' : 'user.add-bonus-title')"
>
@@ -338,6 +350,7 @@
footLabel: 'user.aliases-none'
},
comment: '',
+ debug_modes: [ 'Roundcube', 'Syncroton', 'Chwala' ],
discount: 0,
discount_description: '',
discounts: [],
@@ -353,6 +366,7 @@
walletReload: false,
distlists: [],
domains: [],
+ isAdmin: window.isAdmin,
resources: [],
sku2FA: null,
skus: [],
@@ -552,6 +566,9 @@
this.$root.clearFormValidation($('#email-dialog'))
this.$refs.emailDialog.show()
},
+ hasDebug(mode) {
+ return String(this.user.settings.debug).includes(mode.toLowerCase())
+ },
setMandateState() {
let mandate = this.wallet.mandate
if (mandate && mandate.id) {
@@ -624,6 +641,22 @@
}
})
},
+ submitDebug() {
+ let debug = []
+ $(this.$refs.debugDialog.$el).find('input:checked').each((i, elem) => debug.push(elem.value.toLowerCase()))
+ debug = debug.length ? debug.join() : null
+
+ axios.put('/api/v4/users/' + this.user.id, { debug })
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$refs.debugDialog.hide()
+ this.$toast.success(response.data.message)
+ this.user.settings.debug = debug
+ // we have to update the badge manually (because of some Bootstrap dropdown magic?)
+ $('#button-debug .badge')[debug ? 'removeClass' : 'addClass']('d-none')
+ }
+ })
+ },
submitEmail() {
axios.put('/api/v4/users/' + this.user.id, { external_email: this.external_email })
.then(response => {
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -11,6 +11,7 @@
use App\Utils;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
+use Tests\Browser\Components\Dropdown;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Admin\User as UserPage;
use Tests\Browser\Pages\Dashboard;
@@ -29,6 +30,7 @@
'phone' => '+48123123123',
'external_email' => 'john.doe.external@gmail.com',
'password_expired' => '2020-01-01 10:10:10',
+ 'greylist_policy' => null,
]);
if ($john->isSuspended()) {
User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]);
@@ -91,6 +93,8 @@
'limit_geo' => null,
'organization' => null,
'guam_enabled' => null,
+ 'greylist_enabled' => null,
+ 'greylist_policy' => null,
]);
$event1 = EventLog::createFor($jack, EventLog::TYPE_SUSPENDED, 'Event 1');
@@ -208,7 +212,7 @@
->whenAvailable('@user-settings form', static function (Browser $browser) {
$browser->assertElementsCount('.row', 3)
->assertSeeIn('.row:first-child label', 'Greylisting')
- ->assertSeeIn('.row:first-child .text-success', 'enabled')
+ ->assertSeeIn('.row:first-child .text-danger', 'disabled')
->assertSeeIn('.row:nth-child(2) label', 'IMAP proxy')
->assertSeeIn('.row:nth-child(2) .text-danger', 'disabled')
->assertSeeIn('.row:nth-child(3) label', 'Geo-lockin')
@@ -604,9 +608,10 @@
$john = $this->getTestUser('john@kolab.org');
$browser->visit(new UserPage($john->id))
- ->assertVisible('@user-info #button-suspend')
- ->assertMissing('@user-info #button-unsuspend')
- ->click('@user-info #button-suspend')
+ ->with(new Dropdown('h1 div.dropdown'), static function (Browser $browser) {
+ $browser->assertButton('Actions', 'btn-outline-primary')
+ ->clickDropdownItem('#button-suspend', 'Suspend');
+ })
->with(new Dialog('#suspend-dialog'), static function (Browser $browser) {
$browser->assertSeeIn('@title', 'Suspend')
->assertSeeIn('@button-cancel', 'Cancel')
@@ -615,13 +620,14 @@
->click('@button-action');
})
->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.')
- ->assertSeeIn('@user-info #status span.text-warning', 'Suspended')
- ->assertMissing('@user-info #button-suspend');
+ ->assertSeeIn('@user-info #status span.text-warning', 'Suspended');
$event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
$this->assertSame('test suspend', $event->comment);
- $browser->click('@user-info #button-unsuspend')
+ $browser->with(new Dropdown('h1 div.dropdown'), static function (Browser $browser) {
+ $browser->clickDropdownItem('#button-unsuspend', 'Unsuspend');
+ })
->with(new Dialog('#suspend-dialog'), static function (Browser $browser) {
$browser->assertSeeIn('@title', 'Unsuspend')
->assertSeeIn('@button-cancel', 'Cancel')
@@ -630,8 +636,12 @@
})
->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.')
->assertSeeIn('@user-info #status span.text-success', 'Active')
- ->assertVisible('@user-info #button-suspend')
- ->assertMissing('@user-info #button-unsuspend');
+ ->with(new Dropdown('h1 div.dropdown'), static function (Browser $browser) {
+ $browser->clickDropdownItem('#button-suspend', 'Suspend');
+ })
+ ->with(new Dialog('#suspend-dialog'), static function (Browser $browser) {
+ $browser->click('@button-cancel');
+ });
$event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
$this->assertNull($event->comment);
@@ -647,8 +657,10 @@
$john = $this->getTestUser('john@kolab.org');
$browser->visit(new UserPage($john->id))
- ->assertSeeIn('@user-info #button-resync', 'Resync')
- ->click('@user-info #button-resync')
+ ->with(new Dropdown('h1 div.dropdown'), static function (Browser $browser) {
+ $browser->assertButton('Actions', 'btn-outline-primary')
+ ->clickDropdownItem('#button-resync', 'Resync');
+ })
->assertToast(Toast::TYPE_SUCCESS, "User synchronization has been started.");
});
}
@@ -686,6 +698,61 @@
});
}
+ /**
+ * Test setting debug mode for the user
+ */
+ public function testDebugMode(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $user = $this->getTestUser('userstest1@kolabnow.com');
+
+ $browser->visit(new UserPage($user->id))
+ ->with(new Dropdown('h1 div.dropdown'), static function (Browser $browser) {
+ $browser->click('@button')
+ ->assertMissing('#button-debug > span.badge')
+ ->click('@button')
+ ->clickDropdownItem('#button-debug', 'Debug mode');
+ })
+ ->with(new Dialog('#debug-dialog'), static function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Debug mode')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->assertNotChecked('input#debug_Roundcube')
+ ->assertNotChecked('input#debug_Syncroton')
+ ->assertNotChecked('input#debug_Chwala')
+ ->click('input#debug_Syncroton')
+ ->click('input#debug_Chwala')
+ ->click('@button-action');
+ })
+ ->assertToast(Toast::TYPE_SUCCESS, "User data updated successfully.");
+
+ $this->assertSame('syncroton,chwala', $user->getSetting('debug'));
+
+ $browser->with(new Dropdown('h1 div.dropdown'), static function (Browser $browser) {
+ $browser->click('@button')
+ ->assertSeeIn('#button-debug > span.badge', 'On')
+ ->click('@button')
+ ->clickDropdownItem('#button-debug', 'Debug mode');
+ })
+ ->with(new Dialog('#debug-dialog'), static function (Browser $browser) {
+ $browser->assertNotChecked('input#debug_Roundcube')
+ ->assertChecked('input#debug_Syncroton')
+ ->assertChecked('input#debug_Chwala')
+ ->click('input#debug_Syncroton')
+ ->click('input#debug_Chwala')
+ ->click('@button-action');
+ })
+ ->assertToast(Toast::TYPE_SUCCESS, "User data updated successfully.")
+ ->with(new Dropdown('h1 div.dropdown'), static function (Browser $browser) {
+ $browser->click('@button')
+ ->assertMissing('#button-debug > span.badge')
+ ->click('@button');
+ });
+
+ $this->assertNull($user->getSetting('debug'));
+ });
+ }
+
/**
* Test resetting Geo-Lock for the user
*/
diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php
--- a/src/tests/Feature/Controller/Admin/UsersTest.php
+++ b/src/tests/Feature/Controller/Admin/UsersTest.php
@@ -622,7 +622,7 @@
$this->assertCount(2, $json);
// Test real update
- $post = ['external_email' => 'modified@test.com'];
+ $post = ['external_email' => 'modified@test.com', 'debug' => 'roundcube'];
$response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post);
$response->assertStatus(200);
@@ -632,5 +632,13 @@
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertCount(2, $json);
$this->assertSame('modified@test.com', $user->getSetting('external_email'));
+ $this->assertSame('roundcube', $user->getSetting('debug'));
+
+ // Test unsetting debug
+ $post = ['debug' => null];
+ $response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertNull($user->getSetting('debug'));
}
}
diff --git a/src/tests/Feature/Controller/ConfigTest.php b/src/tests/Feature/Controller/ConfigTest.php
--- a/src/tests/Feature/Controller/ConfigTest.php
+++ b/src/tests/Feature/Controller/ConfigTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature\Controller;
+use App\Http\Controllers\API\V4\ConfigController;
use Tests\TestCase;
class ConfigTest extends TestCase
@@ -24,6 +25,7 @@
$json = $response->json();
$this->assertSame(['kolab4', 'groupware'], $json['kolab-configuration-overlays']);
+ $this->assertArrayNotHasKey('debug', $json);
// Ned has groupware, activesync and 2FA
$response = $this->actingAs($ned)->get('api/v4/config/webmail');
@@ -34,11 +36,25 @@
$this->assertSame(['kolab4', 'activesync', '2fa', 'groupware'], $json['kolab-configuration-overlays']);
// Joe has no groupware subscription
+ $setting = $joe->settings()->updateOrCreate(['key' => 'debug'], ['value' => 'roundcube,syncroton']);
$response = $this->actingAs($joe)->get('api/v4/config/webmail');
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(['kolab4'], $json['kolab-configuration-overlays']);
+ $this->assertSame($setting->value, $json['debug']);
+
+ // Test that the debug mode expires
+ $setting->timestamps = false;
+ $setting->updated_at = now()->subHours(ConfigController::DEBUG_TTL + 1);
+ $setting->save();
+ $response = $this->actingAs($joe)->get('api/v4/config/webmail');
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertArrayNotHasKey('debug', $json);
+ $this->assertNull($joe->getSetting('debug'));
}
}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 4:59 PM (5 h, 41 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828284
Default Alt Text
D5472.1775321984.diff (25 KB)

Event Timeline