diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php
--- a/src/app/Backends/LDAP.php
+++ b/src/app/Backends/LDAP.php
@@ -8,7 +8,12 @@
class LDAP
{
- /** @const array UserSettings used by the backend */
+ /** @const array Group settings used by the backend */
+ public const GROUP_SETTINGS = [
+ 'sender_policy',
+ ];
+
+ /** @const array User settings used by the backend */
public const USER_SETTINGS = [
'first_name',
'last_name',
@@ -570,7 +575,7 @@
if (empty($domain)) {
self::throwException(
$ldap,
- "Failed to update group {$group->email} in LDAP (" . __LINE__ . ")"
+ "Failed to update group {$group->email} in LDAP (group not found)"
);
}
@@ -704,6 +709,10 @@
*/
private static function setGroupAttributes($ldap, Group $group, &$entry)
{
+ $settings = $group->getSettings(['sender_policy']);
+
+ $entry['kolaballowsmtpsender'] = json_decode($settings['sender_policy'] ?: '[]', true);
+
$validMembers = [];
$domain = $group->domain();
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -386,16 +386,6 @@
);
}
- /**
- * Any (additional) properties of this domain.
- *
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
- */
- public function settings()
- {
- return $this->hasMany('App\DomainSetting', 'domain_id');
- }
-
/**
* Suspend this domain.
*
diff --git a/src/app/Group.php b/src/app/Group.php
--- a/src/app/Group.php
+++ b/src/app/Group.php
@@ -4,6 +4,8 @@
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
+use App\Traits\GroupConfigTrait;
+use App\Traits\SettingsTrait;
use App\Traits\UuidIntKeyTrait;
use App\Wallet;
use Illuminate\Database\Eloquent\Model;
@@ -22,6 +24,8 @@
{
use BelongsToTenantTrait;
use EntitleableTrait;
+ use GroupConfigTrait;
+ use SettingsTrait;
use SoftDeletes;
use UuidIntKeyTrait;
diff --git a/src/app/GroupSetting.php b/src/app/GroupSetting.php
new file mode 100644
--- /dev/null
+++ b/src/app/GroupSetting.php
@@ -0,0 +1,30 @@
+belongsTo(\App\Group::class, 'group_id', 'id');
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -142,7 +142,7 @@
}
// Only owner (or admin) has access to the domain
- if (!$this->guard()->user()->canRead($domain)) {
+ if (!$this->guard()->user()->canUpdate($domain)) {
return $this->errorResponse(403);
}
diff --git a/src/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php
--- a/src/app/Http/Controllers/API/V4/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/GroupsController.php
@@ -87,6 +87,37 @@
return response()->json($result);
}
+ /**
+ * Set the group configuration.
+ *
+ * @param int $id Group identifier
+ *
+ * @return \Illuminate\Http\JsonResponse|void
+ */
+ public function setConfig($id)
+ {
+ $group = Group::find($id);
+
+ if (!$this->checkTenant($group)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canUpdate($group)) {
+ return $this->errorResponse(403);
+ }
+
+ $errors = $group->setConfig(request()->input());
+
+ if (!empty($errors)) {
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans('app.distlist-setconfig-success'),
+ ]);
+ }
+
/**
* Display information of a group specified by $id.
*
@@ -111,6 +142,9 @@
$response = array_merge($response, self::groupStatuses($group));
$response['statusInfo'] = self::statusInfo($group);
+ // Group configuration, e.g. sender_policy
+ $response['config'] = $group->getConfig();
+
return response()->json($response);
}
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
@@ -144,7 +144,7 @@
return $this->errorResponse(404);
}
- if (!$this->guard()->user()->canRead($user)) {
+ if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
diff --git a/src/app/Observers/GroupSettingObserver.php b/src/app/Observers/GroupSettingObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/GroupSettingObserver.php
@@ -0,0 +1,51 @@
+key, LDAP::GROUP_SETTINGS)) {
+ \App\Jobs\Group\UpdateJob::dispatch($groupSetting->group_id);
+ }
+ }
+
+ /**
+ * Handle the group setting "updated" event.
+ *
+ * @param \App\GroupSetting $groupSetting Settings object
+ *
+ * @return void
+ */
+ public function updated(GroupSetting $groupSetting)
+ {
+ if (in_array($groupSetting->key, LDAP::GROUP_SETTINGS)) {
+ \App\Jobs\Group\UpdateJob::dispatch($groupSetting->group_id);
+ }
+ }
+
+ /**
+ * Handle the group setting "deleted" event.
+ *
+ * @param \App\GroupSetting $groupSetting Settings object
+ *
+ * @return void
+ */
+ public function deleted(GroupSetting $groupSetting)
+ {
+ if (in_array($groupSetting->key, LDAP::GROUP_SETTINGS)) {
+ \App\Jobs\Group\UpdateJob::dispatch($groupSetting->group_id);
+ }
+ }
+}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -47,6 +47,7 @@
\App\Domain::observe(\App\Observers\DomainObserver::class);
\App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
\App\Group::observe(\App\Observers\GroupObserver::class);
+ \App\GroupSetting::observe(\App\Observers\GroupSettingObserver::class);
\App\OpenVidu\Connection::observe(\App\Observers\OpenVidu\ConnectionObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\PlanPackage::observe(\App\Observers\PlanPackageObserver::class);
diff --git a/src/app/Tenant.php b/src/app/Tenant.php
--- a/src/app/Tenant.php
+++ b/src/app/Tenant.php
@@ -71,16 +71,6 @@
return $this->hasMany('App\Discount');
}
- /**
- * Any (additional) settings of this tenant.
- *
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
- */
- public function settings()
- {
- return $this->hasMany('App\TenantSetting');
- }
-
/**
* SignupInvitations assigned to this tenant.
*
diff --git a/src/app/Traits/GroupConfigTrait.php b/src/app/Traits/GroupConfigTrait.php
new file mode 100644
--- /dev/null
+++ b/src/app/Traits/GroupConfigTrait.php
@@ -0,0 +1,70 @@
+getSetting('sender_policy');
+
+ $config['sender_policy'] = array_filter(
+ $sp ? json_decode($sp, true) : [],
+ function ($item) {
+ // remove the special "-" entry, it's an implementation detail
+ return $item !== '-';
+ }
+ );
+
+ return $config;
+ }
+
+ /**
+ * A helper to update a group configuration.
+ *
+ * @param array $config An array of configuration options
+ *
+ * @return array A list of input validation errors
+ */
+ public function setConfig(array $config): array
+ {
+ $errors = [];
+
+ foreach ($config as $key => $value) {
+ // validate and save SMTP sender policy entries
+ if ($key === 'sender_policy') {
+ if (!is_array($value)) {
+ $value = (array) $value;
+ }
+
+ foreach ($value as $i => $v) {
+ if (!is_string($v)) {
+ $errors[$key][$i] = \trans('validation.sp-entry-invalid');
+ }
+ }
+
+ if (empty($errors[$key])) {
+ // remove empty entries, and '-' entry
+ $value = array_filter($value, function ($item) {
+ return strlen($item) > 0 && $item !== '-';
+ });
+
+ if (!empty($value)) {
+ $value[] = '-'; // Block anyone not on the list
+ }
+
+ $this->setSetting($key, json_encode($value));
+ }
+ } else {
+ $errors[$key] = \trans('validation.invalid-config-parameter');
+ }
+ }
+
+ return $errors;
+ }
+}
diff --git a/src/app/Traits/SettingsTrait.php b/src/app/Traits/SettingsTrait.php
--- a/src/app/Traits/SettingsTrait.php
+++ b/src/app/Traits/SettingsTrait.php
@@ -109,6 +109,16 @@
}
}
+ /**
+ * Any (additional) properties of this domain.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function settings()
+ {
+ return $this->hasMany(self::class . 'Setting');
+ }
+
/**
* Create or update a setting.
*
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -601,16 +601,6 @@
return $matchFound;
}
- /**
- * Any (additional) properties of this user.
- *
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
- */
- public function settings()
- {
- return $this->hasMany('App\UserSetting', 'user_id');
- }
-
/**
* Suspend this domain.
*
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -409,16 +409,6 @@
}
}
- /**
- * Any (additional) properties of this wallet.
- *
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
- */
- public function settings()
- {
- return $this->hasMany('App\WalletSetting');
- }
-
/**
* Retrieve the transactions against this wallet.
*
diff --git a/src/database/migrations/2021_11_05_100000_create_group_settings_table.php b/src/database/migrations/2021_11_05_100000_create_group_settings_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_11_05_100000_create_group_settings_table.php
@@ -0,0 +1,44 @@
+bigIncrements('id');
+ $table->bigInteger('group_id');
+ $table->string('key');
+ $table->text('value');
+ $table->timestamp('created_at')->useCurrent();
+ $table->timestamp('updated_at')->useCurrent();
+
+ $table->foreign('group_id')->references('id')->on('groups')
+ ->onDelete('cascade')->onUpdate('cascade');
+
+ $table->unique(['group_id', 'key']);
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('group_settings');
+ }
+}
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -49,6 +49,7 @@
'distlist-delete-success' => 'Distribution list deleted successfully.',
'distlist-suspend-success' => 'Distribution list suspended successfully.',
'distlist-unsuspend-success' => 'Distribution list unsuspended successfully.',
+ 'distlist-setconfig-success' => 'Distribution list settings updated successfully.',
'domain-create-success' => 'Domain created successfully.',
'domain-delete-success' => 'Domain deleted successfully.',
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
@@ -58,6 +58,10 @@
'list-empty' => "There are no distribution lists in this account.",
'new' => "New distribution list",
'recipients' => "Recipients",
+ 'sender-policy' => "Sender Access List",
+ 'sender-policy-text' => "With this list you can specify who can send mail to the distribution list."
+ . " You can put a complete email address (jane@kolab.org), domain (kolab.org) or suffix (.org) that the sender email address is compared to."
+ . " If the list is empty, mail from anyone is allowed.",
],
'domain' => [
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -139,6 +139,7 @@
'memberislist' => 'A recipient cannot be the same as the list address.',
'listmembersrequired' => 'At least one recipient is required.',
'spf-entry-invalid' => 'The entry format is invalid. Expected a domain name starting with a dot.',
+ 'sp-entry-invalid' => 'The entry format is invalid. Expected an email, domain, or part of it.',
'invalid-config-parameter' => 'The requested configuration parameter is not supported.',
/*
diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue
--- a/src/resources/vue/Admin/Distlist.vue
+++ b/src/resources/vue/Admin/Distlist.vue
@@ -22,7 +22,7 @@
-
+
{{ member }}
@@ -41,6 +41,31 @@
+
+
@@ -48,7 +73,7 @@
export default {
data() {
return {
- list: { members: [] }
+ list: { members: [], config: {} }
}
},
created() {
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue
--- a/src/resources/vue/Admin/Domain.vue
+++ b/src/resources/vue/Admin/Domain.vue
@@ -66,7 +66,7 @@
- {{ domain.config && domain.config.spf_whitelist.length ? domain.config.spf_whitelist.join(', ') : 'none' }}
+ {{ domain.config && domain.config.spf_whitelist.length ? domain.config.spf_whitelist.join(', ') : $t('form.none') }}
diff --git a/src/resources/vue/Distlist/Info.vue b/src/resources/vue/Distlist/Info.vue
--- a/src/resources/vue/Distlist/Info.vue
+++ b/src/resources/vue/Distlist/Info.vue
@@ -12,27 +12,57 @@
{{ $t('distlist.new') }}
@@ -51,7 +81,7 @@
data() {
return {
list_id: null,
- list: { members: [] },
+ list: { members: [], config: {} },
status: {}
}
},
@@ -99,6 +129,15 @@
this.$toast.success(response.data.message)
this.$router.push({ name: 'distlists' })
})
+ },
+ submitSettings() {
+ this.$root.clearFormValidation($('#settings form'))
+ let post = this.list.config
+
+ axios.post('/api/v4/groups/' + this.list_id + '/config', post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ })
}
}
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -54,8 +54,6 @@
}
);
-
-
Route::group(
[
'domain' => \config('app.website_domain'),
@@ -78,6 +76,7 @@
Route::apiResource('groups', API\V4\GroupsController::class);
Route::get('groups/{id}/status', 'API\V4\GroupsController@status');
+ Route::post('groups/{id}/config', 'API\V4\GroupsController@setConfig');
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('skus', API\V4\SkusController::class);
diff --git a/src/tests/Browser/Admin/DistlistTest.php b/src/tests/Browser/Admin/DistlistTest.php
--- a/src/tests/Browser/Admin/DistlistTest.php
+++ b/src/tests/Browser/Admin/DistlistTest.php
@@ -63,6 +63,7 @@
$group->assignToWallet($user->wallets->first());
$group->members = ['test1@gmail.com', 'test2@gmail.com'];
$group->save();
+ $group->setConfig(['sender_policy' => ['test1.com', 'test2.com']]);
$distlist_page = new DistlistPage($group->id);
$user_page = new UserPage($user->id);
@@ -87,6 +88,13 @@
->assertSeeIn('.row:nth-child(3) label', 'Recipients')
->assertSeeIn('.row:nth-child(3) #members', $group->members[0])
->assertSeeIn('.row:nth-child(3) #members', $group->members[1]);
+ })
+ ->assertElementsCount('ul.nav-tabs', 1)
+ ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+ ->with('@distlist-settings form', function (Browser $browser) {
+ $browser->assertElementsCount('.row', 1)
+ ->assertSeeIn('.row:nth-child(1) label', 'Sender Access List')
+ ->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com');
});
// Test invalid group identifier
diff --git a/src/tests/Browser/Components/ListInput.php b/src/tests/Browser/Components/ListInput.php
--- a/src/tests/Browser/Components/ListInput.php
+++ b/src/tests/Browser/Components/ListInput.php
@@ -34,9 +34,9 @@
*/
public function assert($browser)
{
- $browser->assertVisible($this->selector())
- ->assertVisible('@input')
- ->assertVisible('@add-btn');
+ $browser->assertVisible($this->selector)
+ ->assertVisible("{$this->selector} @input")
+ ->assertVisible("{$this->selector} @add-btn");
}
/**
diff --git a/src/tests/Browser/DistlistTest.php b/src/tests/Browser/DistlistTest.php
--- a/src/tests/Browser/DistlistTest.php
+++ b/src/tests/Browser/DistlistTest.php
@@ -125,7 +125,9 @@
->click('button.create-list')
->on(new DistlistInfo())
->assertSeeIn('#distlist-info .card-title', 'New distribution list')
- ->with('@form', function (Browser $browser) {
+ ->assertSeeIn('@nav #tab-general', 'General')
+ ->assertMissing('@nav #tab-settings')
+ ->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertMissing('#status')
->assertSeeIn('div.row:nth-child(1) label', 'Email')
@@ -140,7 +142,7 @@
})
// Test error conditions
->type('#email', 'group-test@kolabnow.com')
- ->click('button[type=submit]')
+ ->click('@general button[type=submit]')
->waitFor('#email + .invalid-feedback')
->assertSeeIn('#email + .invalid-feedback', 'The specified domain is not available.')
->assertFocused('#email')
@@ -153,7 +155,7 @@
$browser->addListEntry('test1@gmail.com')
->addListEntry('test2@gmail.com');
})
- ->click('button[type=submit]')
+ ->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list created successfully.')
->on(new DistlistList())
->assertElementsCount('@table tbody tr', 1);
@@ -162,7 +164,7 @@
$browser->click('@table tr:nth-child(1) a')
->on(new DistlistInfo())
->assertSeeIn('#distlist-info .card-title', 'Distribution list')
- ->with('@form', function (Browser $browser) {
+ ->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
->assertSeeIn('div.row:nth-child(1) span.text-danger', 'Not Ready')
@@ -180,7 +182,7 @@
->with(new ListInput('#members'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
- ->click('button[type=submit]')
+ ->click('@general button[type=submit]')
->waitFor('#members + .invalid-feedback')
->assertSeeIn('#members + .invalid-feedback', 'The specified email address is invalid.')
->assertVisible('#members .input-group:nth-child(4) input.is-invalid')
@@ -189,7 +191,7 @@
->with(new ListInput('#members'), function (Browser $browser) {
$browser->removeListEntry(3)->removeListEntry(2);
})
- ->click('button[type=submit]')
+ ->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list updated successfully.')
->assertMissing('.invalid-feedback')
->on(new DistlistList())
@@ -251,6 +253,53 @@
// TODO: Test all group statuses on the list
}
+ /**
+ * Test distribution list settings
+ */
+ public function testSettings(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $this->addDistlistEntitlement($john);
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+ $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
+ $group->save();
+
+ $this->browse(function ($browser) use ($group) {
+ // Test auto-refresh
+ $browser->visit('/distlist/' . $group->id)
+ ->on(new DistlistInfo())
+ ->assertSeeIn('@nav #tab-general', 'General')
+ ->assertSeeIn('@nav #tab-settings', 'Settings')
+ ->click('@nav #tab-settings')
+ ->with('@settings form', function (Browser $browser) {
+ // Assert form content
+ $browser->assertSeeIn('div.row:nth-child(1) label', 'Sender Access List')
+ ->assertVisible('div.row:nth-child(1) .list-input')
+ ->with(new ListInput('#sender-policy'), function (Browser $browser) {
+ $browser->assertListInputValue([])
+ ->assertValue('@input', '');
+ })
+ ->assertSeeIn('button[type=submit]', 'Submit');
+ })
+ // Test error handling
+ ->with(new ListInput('#sender-policy'), function (Browser $browser) {
+ $browser->addListEntry('test.com');
+ })
+ ->click('@settings button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list settings updated successfully.')
+ ->assertMissing('.invalid-feedback')
+ ->refresh()
+ ->on(new DistlistInfo())
+ ->click('@nav #tab-settings')
+ ->with('@settings form', function (Browser $browser) {
+ $browser->with(new ListInput('#sender-policy'), function (Browser $browser) {
+ $browser->assertListInputValue(['test.com'])
+ ->assertValue('@input', '');
+ });
+ });
+ });
+ }
/**
* Register the beta + distlist entitlements for the user
diff --git a/src/tests/Browser/Pages/Admin/Distlist.php b/src/tests/Browser/Pages/Admin/Distlist.php
--- a/src/tests/Browser/Pages/Admin/Distlist.php
+++ b/src/tests/Browser/Pages/Admin/Distlist.php
@@ -51,7 +51,7 @@
return [
'@app' => '#app',
'@distlist-info' => '#distlist-info',
- '@distlist-config' => '#distlist-config',
+ '@distlist-settings' => '#distlist-settings',
];
}
}
diff --git a/src/tests/Browser/Pages/DistlistInfo.php b/src/tests/Browser/Pages/DistlistInfo.php
--- a/src/tests/Browser/Pages/DistlistInfo.php
+++ b/src/tests/Browser/Pages/DistlistInfo.php
@@ -25,7 +25,7 @@
*/
public function assert($browser)
{
- $browser->waitFor('@form')
+ $browser->waitFor('@general')
->waitUntilMissing('.app-loader');
}
@@ -38,7 +38,9 @@
{
return [
'@app' => '#app',
- '@form' => '#distlist-info form',
+ '@general' => '#general',
+ '@nav' => 'ul.nav-tabs',
+ '@settings' => '#settings',
'@status' => '#status-box',
];
}
diff --git a/src/tests/Browser/Reseller/DistlistTest.php b/src/tests/Browser/Reseller/DistlistTest.php
--- a/src/tests/Browser/Reseller/DistlistTest.php
+++ b/src/tests/Browser/Reseller/DistlistTest.php
@@ -63,6 +63,7 @@
$group->assignToWallet($user->wallets->first());
$group->members = ['test1@gmail.com', 'test2@gmail.com'];
$group->save();
+ $group->setConfig(['sender_policy' => ['test1.com', 'test2.com']]);
$distlist_page = new DistlistPage($group->id);
$user_page = new UserPage($user->id);
@@ -87,6 +88,13 @@
->assertSeeIn('.row:nth-child(3) label', 'Recipients')
->assertSeeIn('.row:nth-child(3) #members', $group->members[0])
->assertSeeIn('.row:nth-child(3) #members', $group->members[1]);
+ })
+ ->assertElementsCount('ul.nav-tabs', 1)
+ ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+ ->with('@distlist-settings form', function (Browser $browser) {
+ $browser->assertElementsCount('.row', 1)
+ ->assertSeeIn('.row:nth-child(1) label', 'Sender Access List')
+ ->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com');
});
// Test invalid group identifier
diff --git a/src/tests/Browser/Reseller/WalletTest.php b/src/tests/Browser/Reseller/WalletTest.php
--- a/src/tests/Browser/Reseller/WalletTest.php
+++ b/src/tests/Browser/Reseller/WalletTest.php
@@ -213,7 +213,7 @@
$browser->waitUntilMissing('.app-loader')
->assertElementsCount('table tbody tr', 10)
->assertMissing('table td.email')
- ->assertSeeIn('#transactions-loader button', 'Load more');
+ ->assertSeeIn('.more-loader button', 'Load more');
foreach ($pages[0] as $idx => $transaction) {
$selector = 'table tbody tr:nth-child(' . ($idx + 1) . ')';
@@ -225,10 +225,10 @@
}
// Load the next page
- $browser->click('#transactions-loader button')
+ $browser->click('.more-loader button')
->waitUntilMissing('.app-loader')
->assertElementsCount('table tbody tr', 12)
- ->assertMissing('#transactions-loader button');
+ ->assertMissing('.more-loader button');
$debitEntry = null;
foreach ($pages[1] as $idx => $transaction) {
diff --git a/src/tests/Feature/Backends/LDAPTest.php b/src/tests/Feature/Backends/LDAPTest.php
--- a/src/tests/Feature/Backends/LDAPTest.php
+++ b/src/tests/Feature/Backends/LDAPTest.php
@@ -127,6 +127,7 @@
$group = $this->getTestGroup('group@kolab.org', [
'members' => ['member1@testldap.com', 'member2@testldap.com']
]);
+ $group->setSetting('sender_policy', '["test.com"]');
// Create the group
LDAP::createGroup($group);
@@ -142,6 +143,7 @@
'groupofuniquenames',
'kolabgroupofuniquenames'
],
+ 'kolaballowsmtpsender' => 'test.com',
'uniquemember' => [
'uid=member1@testldap.com,ou=People,ou=kolab.org,' . $root_dn,
'uid=member2@testldap.com,ou=People,ou=kolab.org,' . $root_dn,
@@ -155,11 +157,13 @@
// Update members
$group->members = ['member3@testldap.com'];
$group->save();
+ $group->setSetting('sender_policy', '["test.com","-"]');
LDAP::updateGroup($group);
// TODO: Should we force this to be always an array?
$expected['uniquemember'] = 'uid=member3@testldap.com,ou=People,ou=kolab.org,' . $root_dn;
+ $expected['kolaballowsmtpsender'] = ['test.com', '-'];
$ldap_group = LDAP::getGroup($group->email);
@@ -172,11 +176,13 @@
// Update members (add non-existing local member, expect it to be aot-removed from the group)
$group->members = ['member3@testldap.com', 'member-local@kolab.org'];
$group->save();
+ $group->setSetting('sender_policy', null);
LDAP::updateGroup($group);
// TODO: Should we force this to be always an array?
$expected['uniquemember'] = 'uid=member3@testldap.com,ou=People,ou=kolab.org,' . $root_dn;
+ $expected['kolaballowsmtpsender'] = null;
$ldap_group = LDAP::getGroup($group->email);
@@ -186,9 +192,9 @@
$this->assertSame(['member3@testldap.com'], $group->fresh()->members);
- // We called save() twice, so we expect two update obs, this is making sure
- // that there's no job executed by the LDAP backend
- Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 2);
+ // We called save() twice, and setSettings() three times,
+ // this is making sure that there's no job executed by the LDAP backend
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 5);
// Delete the domain
LDAP::deleteGroup($group);
@@ -345,6 +351,24 @@
LDAP::updateDomain($domain);
}
+ /**
+ * Test handling update of a non-existing group
+ *
+ * @group ldap
+ */
+ public function testUpdateGroupException(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessageMatches('/group not found/');
+
+ $group = new Group([
+ 'email' => 'test@testldap.com',
+ 'status' => Group::STATUS_NEW | Group::STATUS_ACTIVE,
+ ]);
+
+ LDAP::updateGroup($group);
+ }
+
/**
* Test handling update of a non-existing user
*
diff --git a/src/tests/Feature/Controller/GroupsTest.php b/src/tests/Feature/Controller/GroupsTest.php
--- a/src/tests/Feature/Controller/GroupsTest.php
+++ b/src/tests/Feature/Controller/GroupsTest.php
@@ -117,6 +117,83 @@
$this->assertSame($group->email, $json[0]['email']);
}
+ /**
+ * Test group config update (POST /api/v4/groups//config)
+ */
+ public function testSetConfig(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+
+ // Test unknown group id
+ $post = ['sender_policy' => []];
+ $response = $this->actingAs($john)->post("/api/v4/groups/123/config", $post);
+ $json = $response->json();
+
+ $response->assertStatus(404);
+
+ // Test access by user not being a wallet controller
+ $post = ['sender_policy' => []];
+ $response = $this->actingAs($jack)->post("/api/v4/groups/{$group->id}/config", $post);
+ $json = $response->json();
+
+ $response->assertStatus(403);
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("Access denied", $json['message']);
+ $this->assertCount(2, $json);
+
+ // Test some invalid data
+ $post = ['test' => 1];
+ $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']);
+
+ $group->refresh();
+
+ $this->assertNull($group->getSetting('test'));
+ $this->assertNull($group->getSetting('sender_policy'));
+
+ // Test some valid data
+ $post = ['sender_policy' => ['domain.com']];
+ $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post);
+
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('Distribution list settings updated successfully.', $json['message']);
+
+ $this->assertSame(['sender_policy' => $post['sender_policy']], $group->fresh()->getConfig());
+
+ // Test input validation
+ $post = ['sender_policy' => [5]];
+ $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame(
+ 'The entry format is invalid. Expected an email, domain, or part of it.',
+ $json['errors']['sender_policy'][0]
+ );
+
+ $this->assertSame(['sender_policy' => ['domain.com']], $group->fresh()->getConfig());
+ }
+
/**
* Test fetching group data/profile (GET /api/v4/groups/)
*/
@@ -128,6 +205,7 @@
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
+ $group->setSetting('sender_policy', '["test"]');
// Test unauthorized access to a profile of other user
$response = $this->get("/api/v4/groups/{$group->id}");
@@ -155,6 +233,7 @@
$this->assertArrayHasKey('isSuspended', $json);
$this->assertArrayHasKey('isActive', $json);
$this->assertArrayHasKey('isLdapReady', $json);
+ $this->assertSame(['sender_policy' => ['test']], $json['config']);
}
/**
diff --git a/src/tests/Feature/GroupTest.php b/src/tests/Feature/GroupTest.php
--- a/src/tests/Feature/GroupTest.php
+++ b/src/tests/Feature/GroupTest.php
@@ -42,6 +42,30 @@
$result->assignToWallet($user->wallets->first());
}
+ /**
+ * Test Group::getConfig() and setConfig() methods
+ */
+ public function testConfigTrait(): void
+ {
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ $group->setSetting('sender_policy', '["test","-"]');
+
+ $this->assertSame(['sender_policy' => ['test']], $group->getConfig());
+
+ $result = $group->setConfig(['sender_policy' => [], 'unknown' => false]);
+
+ $this->assertSame(['sender_policy' => []], $group->getConfig());
+ $this->assertSame('[]', $group->getSetting('sender_policy'));
+ $this->assertSame(['unknown' => "The requested configuration parameter is not supported."], $result);
+
+ $result = $group->setConfig(['sender_policy' => ['test']]);
+
+ $this->assertSame(['sender_policy' => ['test']], $group->getConfig());
+ $this->assertSame('["test","-"]', $group->getSetting('sender_policy'));
+ $this->assertSame([], $result);
+ }
+
/**
* Test group status assignment and is*() methods
*/
@@ -189,6 +213,64 @@
$this->assertSame($result->id, $group->id);
}
+ /**
+ * Tests for GroupSettingsTrait functionality and GroupSettingObserver
+ */
+ public function testSettings(): void
+ {
+ Queue::fake();
+ Queue::assertNothingPushed();
+
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 0);
+
+ // Add a setting
+ $group->setSetting('unknown', 'test');
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 0);
+
+ // Add a setting that is synced to LDAP
+ $group->setSetting('sender_policy', '[]');
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1);
+
+ // Note: We test both current group as well as fresh group object
+ // to make sure cache works as expected
+ $this->assertSame('test', $group->getSetting('unknown'));
+ $this->assertSame('[]', $group->fresh()->getSetting('sender_policy'));
+
+ Queue::fake();
+
+ // Update a setting
+ $group->setSetting('unknown', 'test1');
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 0);
+
+ // Update a setting that is synced to LDAP
+ $group->setSetting('sender_policy', '["-"]');
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1);
+
+ $this->assertSame('test1', $group->getSetting('unknown'));
+ $this->assertSame('["-"]', $group->fresh()->getSetting('sender_policy'));
+
+ Queue::fake();
+
+ // Delete a setting (null)
+ $group->setSetting('unknown', null);
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 0);
+
+ // Delete a setting that is synced to LDAP
+ $group->setSetting('sender_policy', null);
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1);
+
+ $this->assertSame(null, $group->getSetting('unknown'));
+ $this->assertSame(null, $group->fresh()->getSetting('sender_policy'));
+ }
+
/**
* Tests for Group::suspend()
*/