Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117738600
D4322.1775155790.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None
D4322.1775155790.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D4322: Code improvements, de-duplication and small optimizations
Attached
Detach File
Event Timeline