Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F118012101
D4057.1775558733.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None
D4057.1775558733.diff
View Options
diff --git a/src/app/Backends/Storage.php b/src/app/Backends/Storage.php
--- a/src/app/Backends/Storage.php
+++ b/src/app/Backends/Storage.php
@@ -64,7 +64,6 @@
public static function fileDownload(Item $file): StreamedResponse
{
$response = new StreamedResponse();
-
$props = $file->getProperties(['name', 'size', 'mimetype']);
// Prepare the file name for the Content-Disposition header
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,16 @@
}
}
}
+
+ // Remove RESTRICTED flag from the wallet owner and all users in the wallet
+ if ($wallet->balance > $wallet->getOriginal('balance') && $wallet->owner && $wallet->owner->isRestricted()) {
+\Log::info('>>>>>>>>>>');
+ User::whereIn('id', $wallet->entitlements()->select('entitleable_id')->where('entitleable_type', User::class))
+ ->each(function ($user) {
+ $user->unrestrict();
+ });
+
+ $wallet->owner->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;
}
@@ -393,6 +395,16 @@
return false;
}
+ /**
+ * 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.
*
@@ -423,6 +435,21 @@
return $this->hasMany(UserPassword::class);
}
+ /**
+ * 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.
*
@@ -517,6 +544,21 @@
$this->save();
}
+ /**
+ * 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.
*
@@ -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
@@ -37,7 +37,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
@@ -1151,6 +1151,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()
*/
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
Details
Attached
Mime Type
text/plain
Expires
Tue, Apr 7, 10:45 AM (16 h, 47 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18833359
Default Alt Text
D4057.1775558733.diff (19 KB)
Attached To
Mode
D4057: User RESTRICTED status
Attached
Detach File
Event Timeline