Page MenuHomePhorge

D4322.1775155790.diff
No OneTemporary

Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None

D4322.1775155790.diff

diff --git a/src/app/Http/Controllers/API/V4/PolicyController.php b/src/app/Http/Controllers/API/V4/PolicyController.php
--- a/src/app/Http/Controllers/API/V4/PolicyController.php
+++ b/src/app/Http/Controllers/API/V4/PolicyController.php
@@ -3,6 +3,8 @@
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
+use App\Policy\RateLimit;
+use App\Policy\RateLimitWhitelist;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
@@ -48,15 +50,13 @@
{
$data = \request()->input();
- $sender = strtolower($data['sender']);
+ list($local, $domain) = \App\Utils::normalizeAddress($data['sender'], true);
- if (strpos($sender, '+') !== false) {
- list($local, $rest) = explode('+', $sender);
- list($rest, $domain) = explode('@', $sender);
- $sender = "{$local}@{$domain}";
+ if (empty($local) || empty($domain)) {
+ return response()->json(['response' => 'HOLD', 'reason' => 'Invalid sender email'], 403);
}
- list($local, $domain) = explode('@', $sender);
+ $sender = $local . '@' . $domain;
if (in_array($sender, \config('app.ratelimit_whitelist', []), true)) {
return response()->json(['response' => 'DUNNO'], 200);
@@ -78,7 +78,7 @@
$user = $alias->user;
}
- if ($user->isDeleted() || $user->isSuspended()) {
+ if (empty($user) || $user->trashed() || $user->isSuspended()) {
// use HOLD, so that it is silent (as opposed to REJECT)
return response()->json(['response' => 'HOLD', 'reason' => 'Sender deleted or suspended'], 403);
}
@@ -93,36 +93,18 @@
return response()->json(['response' => 'DUNNO'], 200);
}
- if ($domain->isDeleted() || $domain->isSuspended()) {
+ if ($domain->trashed() || $domain->isSuspended()) {
// use HOLD, so that it is silent (as opposed to REJECT)
return response()->json(['response' => 'HOLD', 'reason' => 'Sender domain deleted or suspended'], 403);
}
// see if the user or domain is whitelisted
// use ./artisan policy:ratelimit:whitelist:create <email|namespace>
- $whitelist = \App\Policy\RateLimitWhitelist::where(
- [
- 'whitelistable_type' => \App\User::class,
- 'whitelistable_id' => $user->id
- ]
- )->first();
-
- if ($whitelist) {
+ if (RateLimitWhitelist::isListed($user) || RateLimitWhitelist::isListed($domain)) {
return response()->json(['response' => 'DUNNO'], 200);
}
- $whitelist = \App\Policy\RateLimitWhitelist::where(
- [
- 'whitelistable_type' => \App\Domain::class,
- 'whitelistable_id' => $domain->id
- ]
- )->first();
-
- if ($whitelist) {
- return response()->json(['response' => 'DUNNO'], 200);
- }
-
- // user nor domain whitelisted, continue scrutinizing request
+ // user nor domain whitelisted, continue scrutinizing the request
$recipients = $data['recipients'];
sort($recipients);
@@ -135,52 +117,47 @@
$wallet = $user->wallet();
// wait, there is no wallet?
- if (!$wallet) {
+ if (!$wallet || !$wallet->owner) {
return response()->json(['response' => 'HOLD', 'reason' => 'Sender without a wallet'], 403);
}
$owner = $wallet->owner;
// find or create the request
- $request = \App\Policy\RateLimit::where(
- [
- 'recipient_hash' => $recipientHash,
- 'user_id' => $user->id
- ]
- )->where('updated_at', '>=', \Carbon\Carbon::now()->subHour())->first();
+ $request = RateLimit::where('recipient_hash', $recipientHash)
+ ->where('user_id', $user->id)
+ ->where('updated_at', '>=', \Carbon\Carbon::now()->subHour())
+ ->first();
if (!$request) {
- $request = \App\Policy\RateLimit::create(
- [
+ $request = RateLimit::create([
'user_id' => $user->id,
'owner_id' => $owner->id,
'recipient_hash' => $recipientHash,
'recipient_count' => $recipientCount
- ]
- );
-
- // ensure the request has an up to date timestamp
+ ]);
} else {
+ // ensure the request has an up to date timestamp
$request->updated_at = \Carbon\Carbon::now();
$request->save();
}
- // excempt owners that have made at least two payments and currently maintain a positive balance.
- $payments = $wallet->payments
- ->where('amount', '>', 0)
- ->where('status', 'paid');
+ // exempt owners that have made at least two payments and currently maintain a positive balance.
+ if ($wallet->balance > 0) {
+ $payments = $wallet->payments()->where('amount', '>', 0)->where('status', 'paid');
- if ($payments->count() >= 2 && $wallet->balance > 0) {
- return response()->json(['response' => 'DUNNO'], 200);
+ if ($payments->count() >= 2) {
+ return response()->json(['response' => 'DUNNO'], 200);
+ }
}
//
// Examine the rates at which the owner (or its users) is sending
//
- $ownerRates = \App\Policy\RateLimit::where('owner_id', $owner->id)
+ $ownerRates = RateLimit::where('owner_id', $owner->id)
->where('updated_at', '>=', \Carbon\Carbon::now()->subHour());
- if ($ownerRates->count() >= 10) {
+ if (($count = $ownerRates->count()) >= 10) {
$result = [
'response' => 'DEFER_IF_PERMIT',
'reason' => 'The account is at 10 messages per hour, cool down.'
@@ -189,28 +166,14 @@
// automatically suspend (recursively) if 2.5 times over the original limit and younger than two months
$ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
- if ($ownerRates->count() >= 25 && $owner->created_at > $ageThreshold) {
- $wallet->entitlements->each(
- function ($entitlement) {
- if ($entitlement->entitleable_type == \App\Domain::class) {
- $entitlement->entitleable->suspend();
- }
-
- if ($entitlement->entitleable_type == \App\User::class) {
- $entitlement->entitleable->suspend();
- }
- }
- );
+ if ($count >= 25 && $owner->created_at > $ageThreshold) {
+ $owner->suspendAccount();
}
return response()->json($result, 403);
}
- $ownerRates = \App\Policy\RateLimit::where('owner_id', $owner->id)
- ->where('updated_at', '>=', \Carbon\Carbon::now()->subHour())
- ->sum('recipient_count');
-
- if ($ownerRates >= 100) {
+ if (($recipientCount = $ownerRates->sum('recipient_count')) >= 100) {
$result = [
'response' => 'DEFER_IF_PERMIT',
'reason' => 'The account is at 100 recipients per hour, cool down.'
@@ -219,31 +182,21 @@
// automatically suspend if 2.5 times over the original limit and younger than two months
$ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
- if ($ownerRates >= 250 && $owner->created_at > $ageThreshold) {
- $wallet->entitlements->each(
- function ($entitlement) {
- if ($entitlement->entitleable_type == \App\Domain::class) {
- $entitlement->entitleable->suspend();
- }
-
- if ($entitlement->entitleable_type == \App\User::class) {
- $entitlement->entitleable->suspend();
- }
- }
- );
+ if ($recipientCount >= 250 && $owner->created_at > $ageThreshold) {
+ $owner->suspendAccount();
}
return response()->json($result, 403);
}
//
- // Examine the rates at which the user is sending (if not also the owner
+ // Examine the rates at which the user is sending (if not also the owner)
//
if ($user->id != $owner->id) {
- $userRates = \App\Policy\RateLimit::where('user_id', $user->id)
+ $userRates = RateLimit::where('user_id', $user->id)
->where('updated_at', '>=', \Carbon\Carbon::now()->subHour());
- if ($userRates->count() >= 10) {
+ if (($count = $userRates->count()) >= 10) {
$result = [
'response' => 'DEFER_IF_PERMIT',
'reason' => 'User is at 10 messages per hour, cool down.'
@@ -252,18 +205,14 @@
// automatically suspend if 2.5 times over the original limit and younger than two months
$ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
- if ($userRates->count() >= 25 && $user->created_at > $ageThreshold) {
+ if ($count >= 25 && $user->created_at > $ageThreshold) {
$user->suspend();
}
return response()->json($result, 403);
}
- $userRates = \App\Policy\RateLimit::where('user_id', $user->id)
- ->where('updated_at', '>=', \Carbon\Carbon::now()->subHour())
- ->sum('recipient_count');
-
- if ($userRates >= 100) {
+ if (($recipientCount = $userRates->sum('recipient_count')) >= 100) {
$result = [
'response' => 'DEFER_IF_PERMIT',
'reason' => 'User is at 100 recipients per hour, cool down.'
@@ -272,7 +221,7 @@
// automatically suspend if 2.5 times over the original limit
$ageThreshold = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
- if ($userRates >= 250 && $user->created_at > $ageThreshold) {
+ if ($recipientCount >= 250 && $user->created_at > $ageThreshold) {
$user->suspend();
}
diff --git a/src/app/Jobs/WalletCheck.php b/src/app/Jobs/WalletCheck.php
--- a/src/app/Jobs/WalletCheck.php
+++ b/src/app/Jobs/WalletCheck.php
@@ -201,12 +201,7 @@
}
// Suspend the account
- $this->wallet->owner->suspend();
- foreach ($this->wallet->entitlements as $entitlement) {
- if (method_exists($entitlement->entitleable_type, 'suspend')) {
- $entitlement->entitleable->suspend();
- }
- }
+ $this->wallet->owner->suspendAccount();
$this->sendMail(\App\Mail\NegativeBalanceSuspended::class, true);
diff --git a/src/app/Policy/RateLimit.php b/src/app/Policy/RateLimit.php
--- a/src/app/Policy/RateLimit.php
+++ b/src/app/Policy/RateLimit.php
@@ -9,6 +9,7 @@
{
use BelongsToUserTrait;
+ /** @var array<int, string> The attributes that are mass assignable */
protected $fillable = [
'user_id',
'owner_id',
@@ -16,10 +17,6 @@
'recipient_count'
];
+ /** @var string Database table name */
protected $table = 'policy_ratelimit';
-
- public function owner()
- {
- $this->belongsTo(\App\User::class);
- }
}
diff --git a/src/app/Policy/RateLimitWhitelist.php b/src/app/Policy/RateLimitWhitelist.php
--- a/src/app/Policy/RateLimitWhitelist.php
+++ b/src/app/Policy/RateLimitWhitelist.php
@@ -6,11 +6,13 @@
class RateLimitWhitelist extends Model
{
+ /** @var array<int, string> The attributes that are mass assignable */
protected $fillable = [
'whitelistable_id',
'whitelistable_type',
];
+ /** @var string Database table name */
protected $table = 'policy_ratelimit_wl';
/**
@@ -22,4 +24,16 @@
{
return $this->morphTo();
}
+
+ /**
+ * Check whether a specified object is whitelisted.
+ *
+ * @param object $object An object (User, Domain, etc.)
+ */
+ public static function isListed($object): bool
+ {
+ return self::where('whitelistable_type', $object::class)
+ ->where('whitelistable_id', $object->id)
+ ->exists();
+ }
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -646,6 +646,28 @@
}
/**
+ * Suspend all users/domains/groups in this account.
+ */
+ public function suspendAccount(): void
+ {
+ $this->suspend();
+
+ foreach ($this->wallets as $wallet) {
+ $wallet->entitlements()->select('entitleable_id', 'entitleable_type')
+ ->distinct()
+ ->get()
+ ->each(function ($entitlement) {
+ if (
+ defined($entitlement->entitleable_type . '::STATUS_SUSPENDED')
+ && $entitlement->entitleable
+ ) {
+ $entitlement->entitleable->suspend();
+ }
+ });
+ }
+ }
+
+ /**
* Validate the user credentials
*
* @param string $username The username.
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
@@ -1355,6 +1355,49 @@
}
/**
+ * Tests for suspendAccount()
+ */
+ public function testSuspendAccount(): void
+ {
+ $user = $this->getTestUser('UserAccountA@UserAccount.com');
+ $wallet = $user->wallets()->first();
+
+ // No entitlements, expect the wallet owner to be suspended anyway
+ $user->suspendAccount();
+
+ $this->assertTrue($user->fresh()->isSuspended());
+
+ // Add entitlements and more suspendable objects into the wallet
+ $user->unsuspend();
+ $mailbox_sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
+ $domain_sku = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $group_sku = Sku::withEnvTenantContext()->where('title', 'group')->first();
+ $resource_sku = Sku::withEnvTenantContext()->where('title', 'resource')->first();
+ $userB = $this->getTestUser('UserAccountB@UserAccount.com');
+ $userB->assignSku($mailbox_sku, 1, $wallet);
+ $domain = $this->getTestDomain('UserAccount.com', ['type' => \App\Domain::TYPE_PUBLIC]);
+ $domain->assignSku($domain_sku, 1, $wallet);
+ $group = $this->getTestGroup('test-group@UserAccount.com');
+ $group->assignSku($group_sku, 1, $wallet);
+ $resource = $this->getTestResource('test-resource@UserAccount.com');
+ $resource->assignSku($resource_sku, 1, $wallet);
+
+ $this->assertFalse($user->isSuspended());
+ $this->assertFalse($userB->isSuspended());
+ $this->assertFalse($domain->isSuspended());
+ $this->assertFalse($group->isSuspended());
+ $this->assertFalse($resource->isSuspended());
+
+ $user->suspendAccount();
+
+ $this->assertTrue($user->fresh()->isSuspended());
+ $this->assertTrue($userB->fresh()->isSuspended());
+ $this->assertTrue($domain->fresh()->isSuspended());
+ $this->assertTrue($group->fresh()->isSuspended());
+ $this->assertFalse($resource->fresh()->isSuspended());
+ }
+
+ /**
* Tests for UserSettingsTrait::setSettings() and getSetting() and getSettings()
*/
public function testUserSettings(): void

File Metadata

Mime Type
text/plain
Expires
Thu, Apr 2, 6:49 PM (3 d, 2 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820350
Default Alt Text
D4322.1775155790.diff (15 KB)

Event Timeline