Page MenuHomePhorge

D4057.1775502970.diff
No OneTemporary

Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None

D4057.1775502970.diff

diff --git a/src/app/Console/Commands/User/RestrictCommand.php b/src/app/Console/Commands/User/RestrictCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/User/RestrictCommand.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Console\Commands\User;
+
+use App\Console\Command;
+
+class RestrictCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:restrict {user}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Restrict a 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->restrict();
+ }
+}
diff --git a/src/app/Console/Commands/User/UnrestrictCommand.php b/src/app/Console/Commands/User/UnrestrictCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/User/UnrestrictCommand.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Console\Commands\User;
+
+use App\Console\Command;
+
+class UnrestrictCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:unrestrict {user}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Un-restrict a 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->unrestrict();
+ }
+}
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -315,7 +315,6 @@
if ($is_domain) {
$domain = Domain::create([
'namespace' => $domain_name,
- 'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
}
@@ -324,6 +323,7 @@
$user = User::create([
'email' => $login . '@' . $domain_name,
'password' => $request->password,
+ 'status' => User::STATUS_RESTRICTED,
]);
if (!empty($discount)) {
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
@@ -249,6 +249,7 @@
$user = User::create([
'email' => $request->email,
'password' => $request->password,
+ 'status' => $owner->isRestricted() ? User::STATUS_RESTRICTED : 0,
]);
$this->activatePassCode($user);
diff --git a/src/app/Observers/WalletObserver.php b/src/app/Observers/WalletObserver.php
--- a/src/app/Observers/WalletObserver.php
+++ b/src/app/Observers/WalletObserver.php
@@ -2,6 +2,7 @@
namespace App\Observers;
+use App\User;
use App\Wallet;
/**
@@ -107,5 +108,15 @@
}
}
}
+
+ // Remove RESTRICTED flag from the wallet owner and all users in the wallet
+ if ($wallet->balance > $wallet->getOriginal('balance') && $wallet->owner && $wallet->owner->isRestricted()) {
+ $wallet->owner->unrestrict();
+
+ User::whereIn('id', $wallet->entitlements()->select('entitleable_id')->where('entitleable_type', User::class))
+ ->each(function ($user) {
+ $user->unrestrict();
+ });
+ }
}
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -58,6 +58,8 @@
public const STATUS_IMAP_READY = 1 << 5;
// user in "limited feature-set" state
public const STATUS_DEGRADED = 1 << 6;
+ // a restricted user
+ public const STATUS_RESTRICTED = 1 << 7;
/** @var array<int, string> The attributes that are mass assignable */
protected $fillable = [
@@ -154,7 +156,7 @@
*/
public function canDelete($object): bool
{
- if (!method_exists($object, 'wallet')) {
+ if (!is_object($object) || !method_exists($object, 'wallet')) {
return false;
}
@@ -394,6 +396,16 @@
}
/**
+ * Returns whether this user is restricted.
+ *
+ * @return bool
+ */
+ public function isRestricted(): bool
+ {
+ return ($this->status & self::STATUS_RESTRICTED) > 0;
+ }
+
+ /**
* A shortcut to get the user name.
*
* @param bool $fallback Return "<aa.name> User" if there's no name
@@ -424,6 +436,21 @@
}
/**
+ * Restrict this user.
+ *
+ * @return void
+ */
+ public function restrict(): void
+ {
+ if ($this->isRestricted()) {
+ return;
+ }
+
+ $this->status |= User::STATUS_RESTRICTED;
+ $this->save();
+ }
+
+ /**
* Return resources controlled by the current user.
*
* @param bool $with_accounts Include resources assigned to wallets
@@ -518,6 +545,21 @@
}
/**
+ * Un-restrict this user.
+ *
+ * @return void
+ */
+ public function unrestrict(): void
+ {
+ if (!$this->isRestricted()) {
+ return;
+ }
+
+ $this->status ^= User::STATUS_RESTRICTED;
+ $this->save();
+ }
+
+ /**
* Return users controlled by the current user.
*
* @param bool $with_accounts Include users assigned to wallets
@@ -596,6 +638,7 @@
self::STATUS_LDAP_READY,
self::STATUS_IMAP_READY,
self::STATUS_DEGRADED,
+ self::STATUS_RESTRICTED,
];
foreach ($allowed_values as $value) {
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -38,7 +38,7 @@
},
"require-dev": {
"code-lts/doctum": "^5.5.1",
- "laravel/dusk": "~6.22.0",
+ "laravel/dusk": "~7.5.0",
"nunomaduro/larastan": "^2.0",
"phpstan/phpstan": "^1.4",
"phpunit/phpunit": "^9",
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
@@ -424,6 +424,7 @@
'verify-domain' => "Verify domain",
'degraded' => "Degraded",
'deleted' => "Deleted",
+ 'restricted' => "Restricted",
'suspended' => "Suspended",
'notready' => "Not Ready",
'active' => "Active",
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
@@ -26,6 +26,7 @@
<div class="col-sm-8">
<span class="form-control-plaintext" id="status">
<span :class="$root.statusClass(user)">{{ $root.statusText(user) }}</span>
+ <span v-if="user.isRestricted" class="badge bg-primary rounded-pill ms-1">{{ $t('status.restricted') }}</span>
</span>
</div>
</div>
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
@@ -107,6 +107,7 @@
->assertSeeIn('.row:nth-child(2) #userid', "{$jack->id} ({$jack->created_at})")
->assertSeeIn('.row:nth-child(3) label', 'Status')
->assertSeeIn('.row:nth-child(3) #status span.text-success', 'Active')
+ ->assertDontSeeIn('.row:nth-child(3) #status', 'Restricted')
->assertSeeIn('.row:nth-child(4) label', 'First Name')
->assertSeeIn('.row:nth-child(4) #first_name', 'Jack')
->assertSeeIn('.row:nth-child(5) label', 'Last Name')
@@ -562,9 +563,11 @@
$user = $this->getTestUser('userstest1@kolabnow.com');
$sku2fa = Sku::withEnvTenantContext()->where('title', '2fa')->first();
$user->assignSku($sku2fa);
+ $user->restrict();
SecondFactor::seed('userstest1@kolabnow.com');
$browser->visit(new UserPage($user->id))
+ ->assertSeeIn('@user-info #status', 'Restricted')
->click('@nav #tab-subscriptions')
->with('@user-subscriptions', function (Browser $browser) use ($sku2fa) {
$browser->waitFor('#reset2fa')
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -330,7 +330,8 @@
->assertMissing('#password_confirmation')
->waitFor('#password-link code')
->assertSeeIn('#password-link code', $link)
- ->assertSeeIn('#password-link div.form-text', "Press Submit to activate the link");
+ ->assertSeeIn('#password-link div.form-text', "Press Submit to activate the link")
+ ->pause(100);
// Test copy to clipboard
/* TODO: Figure out how to give permission to do this operation
diff --git a/src/tests/Feature/Console/User/RestrictTest.php b/src/tests/Feature/Console/User/RestrictTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/User/RestrictTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Tests\Feature\Console\User;
+
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class RestrictTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('user-restrict-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('user-restrict-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Non-existing user
+ $code = \Artisan::call("user:restrict unknown@unknown.org");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("User not found.", $output);
+
+ // Create a user account for degrade
+ $user = $this->getTestUser('user-restrict-test@kolabnow.com');
+
+ $this->assertFalse($user->isRestricted());
+
+ $code = \Artisan::call("user:restrict {$user->email}");
+ $output = trim(\Artisan::output());
+
+ $user->refresh();
+
+ $this->assertTrue($user->isRestricted());
+ $this->assertSame('', $output);
+ $this->assertSame(0, $code);
+ }
+}
diff --git a/src/tests/Feature/Console/User/ResyncTest.php b/src/tests/Feature/Console/User/ResyncTest.php
--- a/src/tests/Feature/Console/User/ResyncTest.php
+++ b/src/tests/Feature/Console/User/ResyncTest.php
@@ -91,7 +91,7 @@
]);
// Remove all deleted users except one, to not interfere
- User::withTrashed()->whereNotIn('id', [$user->id])->forceDelete();
+ User::withTrashed()->whereNotNull('deleted_at')->whereNotIn('id', [$user->id])->forceDelete();
// Test run for all deleted users
$code = \Artisan::call("user:resync --deleted-only");
diff --git a/src/tests/Feature/Console/User/UnrestrictTest.php b/src/tests/Feature/Console/User/UnrestrictTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/User/UnrestrictTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Tests\Feature\Console\User;
+
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class UnrestrictTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('user-restrict-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('user-restrict-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Non-existing user
+ $code = \Artisan::call("user:unrestrict unknown@unknown.org");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("User not found.", $output);
+
+ $user = $this->getTestUser('user-restrict-test@kolabnow.com', ['status' => \App\User::STATUS_RESTRICTED]);
+
+ $this->assertTrue($user->isRestricted());
+
+ $code = \Artisan::call("user:unrestrict {$user->email}");
+ $output = trim(\Artisan::output());
+
+ $user->refresh();
+
+ $this->assertFalse($user->isRestricted());
+ $this->assertSame('', $output);
+ $this->assertSame(0, $code);
+ }
+}
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -614,6 +614,7 @@
$this->assertNotEmpty($user);
$this->assertSame($identity, $user->email);
+ $this->assertTrue($user->isRestricted());
// Check if the code has been updated and soft-deleted
$this->assertTrue($code->trashed());
@@ -746,6 +747,7 @@
$user = User::where('email', $login . '@' . $domain)->first();
$this->assertNotEmpty($user);
+ $this->assertTrue($user->isRestricted());
// Check user settings
$this->assertSame($user_data['email'], $user->getSetting('external_email'));
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -893,6 +893,7 @@
$this->assertSame('John2', $user->getSetting('first_name'));
$this->assertSame('Doe2', $user->getSetting('last_name'));
$this->assertSame('TestOrg', $user->getSetting('organization'));
+ $this->assertFalse($user->isRestricted());
/** @var \App\UserAlias[] $aliases */
$aliases = $user->aliases()->orderBy('alias')->get();
$this->assertCount(2, $aliases);
@@ -963,6 +964,29 @@
$response = $this->actingAs($user)->post("/api/v4/users", []);
$response->assertStatus(403);
+
+ // Test that creating a user in a restricted account creates a restricted user
+ $package_domain = Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $owner = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $domain = $this->getTestDomain(
+ 'userscontroller.com',
+ ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL]
+ );
+ $domain->assignPackage($package_domain, $owner);
+ $owner->restrict();
+
+ $post = [
+ 'password' => 'simple123',
+ 'password_confirmation' => 'simple123',
+ 'email' => 'UsersControllerTest2@userscontroller.com',
+ 'package' => $package_kolab->id,
+ ];
+
+ $response = $this->actingAs($owner)->post("/api/v4/users", $post);
+ $response->assertStatus(200);
+
+ $user = User::where('email', 'UsersControllerTest1@userscontroller.com')->first();
+ $this->assertTrue($user->isRestricted());
}
/**
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -1152,6 +1152,42 @@
}
/**
+ * Test user account restrict() and unrestrict()
+ */
+ public function testRestrictAndUnrestrict(): void
+ {
+ Queue::fake();
+
+ // Test an account with users, domain
+ $user = $this->getTestUser('UserAccountA@UserAccount.com');
+
+ $this->assertFalse($user->isRestricted());
+
+ $user->restrict();
+
+ $this->assertTrue($user->fresh()->isRestricted());
+
+ Queue::assertPushed(
+ \App\Jobs\User\UpdateJob::class,
+ function ($job) use ($user) {
+ return TestCase::getObjectProperty($job, 'userId') == $user->id;
+ });
+
+ Queue::fake(); // reset queue state
+
+ $user->refresh();
+ $user->unrestrict();
+
+ $this->assertFalse($user->fresh()->isRestricted());
+
+ Queue::assertPushed(
+ \App\Jobs\User\UpdateJob::class,
+ function ($job) use ($user) {
+ return TestCase::getObjectProperty($job, 'userId') == $user->id;
+ });
+ }
+
+ /**
* Tests for AliasesTrait::setAliases()
*/
public function testSetAliases(): void
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -86,6 +86,35 @@
$this->assertFalse($user->isDegraded());
$this->assertNull($wallet->getSetting('balance_negative_since'));
+ // Test un-restricting users on balance change
+ $this->deleteTestUser('UserWallet1@UserWallet.com');
+ $owner = $this->getTestUser('UserWallet1@UserWallet.com');
+ $user1 = $this->getTestUser('UserWallet2@UserWallet.com');
+ $user2 = $this->getTestUser('UserWallet3@UserWallet.com');
+ $package = Package::withEnvTenantContext()->where('title', 'lite')->first();
+ $owner->assignPackage($package, $user1);
+ $owner->assignPackage($package, $user2);
+ $wallet = $owner->wallets()->first();
+
+ $owner->restrict();
+ $user1->restrict();
+ $user2->restrict();
+
+ $this->assertTrue($owner->isRestricted());
+ $this->assertTrue($user1->isRestricted());
+ $this->assertTrue($user2->isRestricted());
+
+ Queue::fake();
+
+ $wallet->balance = 100;
+ $wallet->save();
+
+ $this->assertFalse($owner->fresh()->isRestricted());
+ $this->assertFalse($user1->fresh()->isRestricted());
+ $this->assertFalse($user2->fresh()->isRestricted());
+
+ Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 3);
+
// TODO: Test group account and unsuspending domain/members/groups
}
diff --git a/src/tests/Unit/UserTest.php b/src/tests/Unit/UserTest.php
--- a/src/tests/Unit/UserTest.php
+++ b/src/tests/Unit/UserTest.php
@@ -78,6 +78,7 @@
User::STATUS_IMAP_READY,
User::STATUS_LDAP_READY,
User::STATUS_DEGRADED,
+ User::STATUS_RESTRICTED,
];
$users = \App\Utils::powerSet($statuses);
@@ -97,6 +98,7 @@
$this->assertTrue($user->isLdapReady() === in_array(User::STATUS_LDAP_READY, $user_statuses));
$this->assertTrue($user->isImapReady() === in_array(User::STATUS_IMAP_READY, $user_statuses));
$this->assertTrue($user->isDegraded() === in_array(User::STATUS_DEGRADED, $user_statuses));
+ $this->assertTrue($user->isRestricted() === in_array(User::STATUS_RESTRICTED, $user_statuses));
}
}

File Metadata

Mime Type
text/plain
Expires
Mon, Apr 6, 7:16 PM (1 h, 7 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18839003
Default Alt Text
D4057.1775502970.diff (19 KB)

Event Timeline