diff --git a/extras/kolab_policy_greylist.py b/extras/kolab_policy_greylist.py index 6a99abdf..c26186ee 100755 --- a/extras/kolab_policy_greylist.py +++ b/extras/kolab_policy_greylist.py @@ -1,79 +1,79 @@ #!/usr/bin/python3 """ An example implementation of a policy service. """ import json import time import sys import requests def read_request_input(): """ Read a single policy request from sys.stdin, and return a dictionary containing the request. """ start_time = time.time() policy_request = {} end_of_request = False while not end_of_request: if (time.time() - start_time) >= 10: sys.exit(0) request_line = sys.stdin.readline() if request_line.strip() == '': if 'request' in policy_request: end_of_request = True else: request_line = request_line.strip() request_key = request_line.split('=')[0] request_value = '='.join(request_line.split('=')[1:]) policy_request[request_key] = request_value return policy_request if __name__ == "__main__": - URL = 'https://services.kolabnow.com/api/webhooks/greylist' + URL = 'https://services.kolabnow.com/api/webhooks/policy/greylist' # Start the work while True: REQUEST = read_request_input() try: RESPONSE = requests.post( URL, data=REQUEST, verify=True ) # pylint: disable=broad-except except Exception: print("action=DEFER_IF_PERMIT Temporary error, try again later.") sys.exit(1) try: R = json.loads(RESPONSE.text) # pylint: disable=broad-except except Exception: sys.exit(1) if 'prepend' in R: for prepend in R['prepend']: print("action=PREPEND {0}".format(prepend)) if RESPONSE.ok: print("action={0}\n".format(R['response'])) sys.stdout.flush() else: print("action={0} {1}\n".format(R['response'], R['reason'])) sys.stdout.flush() sys.exit(0) diff --git a/extras/kolab_policy_greylist.py b/extras/kolab_policy_ratelimit.py similarity index 94% copy from extras/kolab_policy_greylist.py copy to extras/kolab_policy_ratelimit.py index 6a99abdf..b459b257 100755 --- a/extras/kolab_policy_greylist.py +++ b/extras/kolab_policy_ratelimit.py @@ -1,79 +1,79 @@ #!/usr/bin/python3 """ -An example implementation of a policy service. +This policy applies rate limitations """ import json import time import sys import requests def read_request_input(): """ Read a single policy request from sys.stdin, and return a dictionary containing the request. """ start_time = time.time() policy_request = {} end_of_request = False while not end_of_request: if (time.time() - start_time) >= 10: sys.exit(0) request_line = sys.stdin.readline() if request_line.strip() == '': if 'request' in policy_request: end_of_request = True else: request_line = request_line.strip() request_key = request_line.split('=')[0] request_value = '='.join(request_line.split('=')[1:]) policy_request[request_key] = request_value return policy_request if __name__ == "__main__": - URL = 'https://services.kolabnow.com/api/webhooks/greylist' + URL = 'https://services.kolabnow.com/api/webhooks/policy/ratelimit' # Start the work while True: REQUEST = read_request_input() try: RESPONSE = requests.post( URL, data=REQUEST, verify=True ) # pylint: disable=broad-except except Exception: print("action=DEFER_IF_PERMIT Temporary error, try again later.") sys.exit(1) try: R = json.loads(RESPONSE.text) # pylint: disable=broad-except except Exception: sys.exit(1) if 'prepend' in R: for prepend in R['prepend']: print("action=PREPEND {0}".format(prepend)) if RESPONSE.ok: print("action={0}\n".format(R['response'])) sys.stdout.flush() else: print("action={0} {1}\n".format(R['response'], R['reason'])) sys.stdout.flush() sys.exit(0) diff --git a/extras/kolab_policy_spf.py b/extras/kolab_policy_spf.py index 26cbae3f..d98baac8 100755 --- a/extras/kolab_policy_spf.py +++ b/extras/kolab_policy_spf.py @@ -1,80 +1,80 @@ #!/usr/bin/python3 """ This is the implementation of a (postfix) MTA policy service to enforce the Sender Policy Framework. """ import json import time import sys import requests def read_request_input(): """ Read a single policy request from sys.stdin, and return a dictionary containing the request. """ start_time = time.time() policy_request = {} end_of_request = False while not end_of_request: if (time.time() - start_time) >= 10: sys.exit(0) request_line = sys.stdin.readline() if request_line.strip() == '': if 'request' in policy_request: end_of_request = True else: request_line = request_line.strip() request_key = request_line.split('=')[0] request_value = '='.join(request_line.split('=')[1:]) policy_request[request_key] = request_value return policy_request if __name__ == "__main__": - URL = 'https://services.kolabnow.com/api/webhooks/spf' + URL = 'https://services.kolabnow.com/api/webhooks/policy/spf' # Start the work while True: REQUEST = read_request_input() try: RESPONSE = requests.post( URL, data=REQUEST, verify=True ) # pylint: disable=broad-except except Exception: print("action=DEFER_IF_PERMIT Temporary error, try again later.") sys.exit(1) try: R = json.loads(RESPONSE.text) # pylint: disable=broad-except except Exception: sys.exit(1) if 'prepend' in R: for prepend in R['prepend']: print("action=PREPEND {0}".format(prepend)) if RESPONSE.ok: print("action={0}\n".format(R['response'])) sys.stdout.flush() else: print("action={0} {1}\n".format(R['response'], R['reason'])) sys.stdout.flush() sys.exit(0) diff --git a/src/app/Http/Controllers/API/V4/PolicyController.php b/src/app/Http/Controllers/API/V4/PolicyController.php index f22e17e9..e1ab7ef3 100644 --- a/src/app/Http/Controllers/API/V4/PolicyController.php +++ b/src/app/Http/Controllers/API/V4/PolicyController.php @@ -1,168 +1,228 @@ input(); list($local, $domainName) = explode('@', $data['recipient']); - $request = new \App\Greylist\Request($data); + $request = new \App\Policy\Greylist\Request($data); $shouldDefer = $request->shouldDefer(); if ($shouldDefer) { return response()->json( ['response' => 'DEFER_IF_PERMIT', 'reason' => "Greylisted for 5 minutes. Try again later."], 403 ); } $prependGreylist = $request->headerGreylist(); $result = [ 'response' => 'DUNNO', 'prepend' => [$prependGreylist] ]; return response()->json($result, 200); } + /* + * Apply a sensible rate limitation to a request. + * + * @return \Illuminate\Http\JsonResponse + */ + public function ratelimit() + { + /* + $data = [ + 'instance' => 'test.local.instance', + 'protocol_state' => 'RCPT', + 'sender' => 'sender@spf-pass.kolab.org', + 'client_name' => 'mx.kolabnow.com', + 'client_address' => '212.103.80.148', + 'recipient' => $this->domainOwner->email + ]; + + $response = $this->post('/api/webhooks/spf', $data); + */ + + $data = \request()->input(); + + // TODO: normalize sender address + $sender = strtolower($data['sender']); + + $alias = \App\UserAlias::where('alias', $sender)->first(); + + if (!$alias) { + $user = \App\User::where('email', $sender)->first(); + + if (!$user) { + // what's the situation here? + } + } else { + $user = $alias->user; + } + + // TODO time-limit + $userRates = \App\Policy\Ratelimit::where('user_id', $user->id); + + // TODO message vs. recipient limit + if ($userRates->count() > 10) { + // TODO + } + + // this is the wallet to which the account is billed + $wallet = $user->wallet; + + // TODO: consider $wallet->payments; + + $owner = $wallet->user; + + // TODO time-limit + $ownerRates = \App\Policy\Ratelimit::where('owner_id', $owner->id); + + // TODO message vs. recipient limit (w/ user counts) + if ($ownerRates->count() > 10) { + // TODO + } + } + /* * Apply the sender policy framework to a request. * * @return \Illuminate\Http\JsonResponse */ public function senderPolicyFramework() { $data = \request()->input(); list($netID, $netType) = \App\Utils::getNetFromAddress($data['client_address']); list($senderLocal, $senderDomain) = explode('@', $data['sender']); // This network can not be recognized. if (!$netID) { return response()->json( [ 'response' => 'DEFER_IF_PERMIT', 'reason' => 'Temporary error. Please try again later.' ], 403 ); } // Compose the cache key we want. $cacheKey = "{$netType}_{$netID}_{$senderDomain}"; - $result = \App\SPF\Cache::get($cacheKey); + $result = \App\Policy\SPF\Cache::get($cacheKey); if (!$result) { $environment = new \SPFLib\Check\Environment( $data['client_address'], $data['client_name'], $data['sender'] ); $result = (new \SPFLib\Checker())->check($environment); - \App\SPF\Cache::set($cacheKey, serialize($result)); + \App\Policy\SPF\Cache::set($cacheKey, serialize($result)); } else { $result = unserialize($result); } $fail = false; switch ($result->getCode()) { case \SPFLib\Check\Result::CODE_ERROR_PERMANENT: $fail = true; $prependSPF = "Received-SPF: Permerror"; break; case \SPFLib\Check\Result::CODE_ERROR_TEMPORARY: $prependSPF = "Received-SPF: Temperror"; break; case \SPFLib\Check\Result::CODE_FAIL: $fail = true; $prependSPF = "Received-SPF: Fail"; break; case \SPFLib\Check\Result::CODE_SOFTFAIL: $prependSPF = "Received-SPF: Softfail"; break; case \SPFLib\Check\Result::CODE_NEUTRAL: $prependSPF = "Received-SPF: Neutral"; break; case \SPFLib\Check\Result::CODE_PASS: $prependSPF = "Received-SPF: Pass"; break; case \SPFLib\Check\Result::CODE_NONE: $prependSPF = "Received-SPF: None"; break; } $prependSPF .= " identity=mailfrom;"; $prependSPF .= " client-ip={$data['client_address']};"; $prependSPF .= " helo={$data['client_name']};"; $prependSPF .= " envelope-from={$data['sender']};"; if ($fail) { // TODO: check the recipient's policy, such as using barracuda for anti-spam and anti-virus as a relay for // inbound mail to a local recipient address. $objects = \App\Utils::findObjectsByRecipientAddress($data['recipient']); if (!empty($objects)) { // check if any of the recipient objects have whitelisted the helo, first one wins. foreach ($objects as $object) { if (method_exists($object, 'senderPolicyFrameworkWhitelist')) { $result = $object->senderPolicyFrameworkWhitelist($data['client_name']); if ($result) { $response = [ 'response' => 'DUNNO', 'prepend' => ["Received-SPF: Pass Check skipped at recipient's discretion"], 'reason' => 'HELO name whitelisted' ]; return response()->json($response, 200); } } } } $result = [ 'response' => 'REJECT', 'prepend' => [$prependSPF], 'reason' => "Prohibited by Sender Policy Framework" ]; return response()->json($result, 403); } $result = [ 'response' => 'DUNNO', 'prepend' => [$prependSPF], 'reason' => "Don't know" ]; return response()->json($result, 200); } } diff --git a/src/app/Greylist/Connect.php b/src/app/Policy/Greylist/Connect.php similarity index 97% rename from src/app/Greylist/Connect.php rename to src/app/Policy/Greylist/Connect.php index 0c6e4662..6bd1b2b1 100644 --- a/src/app/Greylist/Connect.php +++ b/src/app/Policy/Greylist/Connect.php @@ -1,62 +1,62 @@ recipient_type == \App\Domain::class) { return $this->recipient; } return null; } // determine if the sender is a penpal of the recipient. public function isPenpal() { return false; } public function user() { if ($this->recipient_type == \App\User::class) { return $this->recipient; } return null; } public function net() { return $this->morphTo(); } public function recipient() { return $this->morphTo(); } } diff --git a/src/app/Greylist/Request.php b/src/app/Policy/Greylist/Request.php similarity index 99% rename from src/app/Greylist/Request.php rename to src/app/Policy/Greylist/Request.php index ef95e75f..493ca04d 100644 --- a/src/app/Greylist/Request.php +++ b/src/app/Policy/Greylist/Request.php @@ -1,299 +1,299 @@ request = $request; if (array_key_exists('timestamp', $this->request)) { $this->timestamp = \Carbon\Carbon::parse($this->request['timestamp']); } else { $this->timestamp = \Carbon\Carbon::now(); } } public function headerGreylist() { if ($this->whitelist) { if ($this->whitelist->sender_local) { return sprintf( "Received-Greylist: sender %s whitelisted since %s", $this->sender, $this->whitelist->created_at->toDateString() ); } return sprintf( "Received-Greylist: domain %s from %s whitelisted since %s (UTC)", $this->senderDomain, $this->request['client_address'], $this->whitelist->created_at->toDateTimeString() ); } $connect = $this->findConnectsCollection()->orderBy('created_at')->first(); if ($connect) { return sprintf( "Received-Greylist: greylisted from %s until %s.", $connect->created_at, $this->timestamp ); } return "Received-Greylist: no opinion here"; } public function shouldDefer() { $deferIfPermit = true; list($this->netID, $this->netType) = \App\Utils::getNetFromAddress($this->request['client_address']); if (!$this->netID) { return true; } $recipient = $this->recipientFromRequest(); $this->sender = $this->senderFromRequest(); list($this->senderLocal, $this->senderDomain) = explode('@', $this->sender); $entry = $this->findConnectsCollectionRecent()->orderBy('updated_at')->first(); if (!$entry) { // purge all entries to avoid a unique constraint violation. $this->findConnectsCollection()->delete(); $entry = \App\Greylist\Connect::create( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_hash' => $this->recipientHash, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType, 'connect_count' => 1, 'created_at' => $this->timestamp, 'updated_at' => $this->timestamp ] ); } // see if all recipients and their domains are opt-outs $enabled = false; if ($recipient) { $setting = \App\Greylist\Setting::where( [ 'object_id' => $this->recipientID, 'object_type' => $this->recipientType, 'key' => 'greylist_enabled' ] )->first(); if (!$setting) { $setting = \App\Greylist\Setting::where( [ 'object_id' => $recipient->domain()->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled' ] )->first(); if (!$setting) { $enabled = true; } else { if ($setting->{'value'} !== 'false') { $enabled = true; } } } else { if ($setting->{'value'} !== 'false') { $enabled = true; } } } else { $enabled = true; } // the following block is to maintain statistics and state ... $entries = \App\Greylist\Connect::where( [ 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType ] ) ->whereDate('updated_at', '>=', $this->timestamp->copy()->subDays(7)); // determine if the sender domain is a whitelist from this network $this->whitelist = \App\Greylist\Whitelist::where( [ 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType ] )->first(); if ($this->whitelist) { if ($this->whitelist->updated_at < $this->timestamp->copy()->subMonthsWithoutOverflow(1)) { $this->whitelist->delete(); } else { $this->whitelist->updated_at = $this->timestamp; $this->whitelist->save(['timestamps' => false]); $entries->update( [ 'greylisting' => false, 'updated_at' => $this->timestamp ] ); return false; } } else { if ($entries->count() >= 5) { $this->whitelist = \App\Greylist\Whitelist::create( [ 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'created_at' => $this->timestamp, 'updated_at' => $this->timestamp ] ); $entries->update( [ 'greylisting' => false, 'updated_at' => $this->timestamp ] ); } } // TODO: determine if the sender (individual) is a whitelist // TODO: determine if the sender is a penpal of any of the recipients. First recipient wins. if (!$enabled) { return false; } // determine if the sender, net and recipient combination has existed before, for each recipient // any one recipient matching should supersede the other recipients not having matched $connect = \App\Greylist\Connect::where( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType, ] ) ->whereDate('updated_at', '>=', $this->timestamp->copy()->subMonthsWithoutOverflow(1)) ->orderBy('updated_at') ->first(); if (!$connect) { $connect = \App\Greylist\Connect::create( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType, 'connect_count' => 0, 'created_at' => $this->timestamp, 'updated_at' => $this->timestamp ] ); } $connect->connect_count += 1; // TODO: The period of time for which the greylisting persists is configurable. if ($connect->created_at < $this->timestamp->copy()->subMinutes(5)) { $deferIfPermit = false; $connect->greylisting = false; } $connect->save(); return $deferIfPermit; } private function findConnectsCollection() { $collection = \App\Greylist\Connect::where( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType ] ); return $collection; } private function findConnectsCollectionRecent() { return $this->findConnectsCollection() ->where('updated_at', '>=', $this->timestamp->copy()->subDays(7)); } private function recipientFromRequest() { $recipients = \App\Utils::findObjectsByRecipientAddress($this->request['recipient']); if (sizeof($recipients) > 1) { \Log::warning( "Only taking the first recipient from the request in to account for {$this->request['recipient']}" ); } if (count($recipients) >= 1) { $recipient = $recipients[0]; $this->recipientID = $recipient->id; $this->recipientType = get_class($recipient); } else { $recipient = null; } $this->recipientHash = hash('sha256', $this->request['recipient']); return $recipient; } public function senderFromRequest() { return \App\Utils::normalizeAddress($this->request['sender']); } } diff --git a/src/app/Greylist/Setting.php b/src/app/Policy/Greylist/Setting.php similarity index 88% rename from src/app/Greylist/Setting.php rename to src/app/Policy/Greylist/Setting.php index e12ec630..ec223ef8 100644 --- a/src/app/Greylist/Setting.php +++ b/src/app/Policy/Greylist/Setting.php @@ -1,17 +1,17 @@ 'api', 'prefix' => $prefix . 'api/auth' ], function ($router) { Route::post('login', 'API\AuthController@login'); Route::group( ['middleware' => 'auth:api'], function ($router) { Route::get('info', 'API\AuthController@info'); Route::post('logout', 'API\AuthController@logout'); Route::post('refresh', 'API\AuthController@refresh'); } ); } ); Route::group( [ 'domain' => \config('app.domain'), 'middleware' => 'api', 'prefix' => $prefix . 'api/auth' ], function ($router) { Route::post('password-reset/init', 'API\PasswordResetController@init'); Route::post('password-reset/verify', 'API\PasswordResetController@verify'); Route::post('password-reset', 'API\PasswordResetController@reset'); Route::get('signup/plans', 'API\SignupController@plans'); Route::post('signup/init', 'API\SignupController@init'); Route::post('signup/verify', 'API\SignupController@verify'); Route::post('signup', 'API\SignupController@signup'); } ); Route::group( [ 'domain' => \config('app.domain'), 'middleware' => 'auth:api', 'prefix' => $prefix . 'api/v4' ], function () { Route::apiResource('domains', API\V4\DomainsController::class); Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm'); Route::get('domains/{id}/status', 'API\V4\DomainsController@status'); Route::post('domains/{id}/config', 'API\V4\DomainsController@setConfig'); Route::apiResource('entitlements', API\V4\EntitlementsController::class); Route::apiResource('packages', API\V4\PackagesController::class); Route::apiResource('skus', API\V4\SkusController::class); Route::apiResource('users', API\V4\UsersController::class); Route::post('users/{id}/config', 'API\V4\UsersController@setConfig'); Route::get('users/{id}/skus', 'API\V4\SkusController@userSkus'); Route::get('users/{id}/status', 'API\V4\UsersController@status'); Route::apiResource('wallets', API\V4\WalletsController::class); Route::get('wallets/{id}/transactions', 'API\V4\WalletsController@transactions'); Route::get('wallets/{id}/receipts', 'API\V4\WalletsController@receipts'); Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\WalletsController@receiptDownload'); Route::post('payments', 'API\V4\PaymentsController@store'); //Route::delete('payments', 'API\V4\PaymentsController@cancel'); Route::get('payments/mandate', 'API\V4\PaymentsController@mandate'); Route::post('payments/mandate', 'API\V4\PaymentsController@mandateCreate'); Route::put('payments/mandate', 'API\V4\PaymentsController@mandateUpdate'); Route::delete('payments/mandate', 'API\V4\PaymentsController@mandateDelete'); Route::get('payments/methods', 'API\V4\PaymentsController@paymentMethods'); Route::get('payments/pending', 'API\V4\PaymentsController@payments'); Route::get('payments/has-pending', 'API\V4\PaymentsController@hasPayments'); Route::get('openvidu/rooms', 'API\V4\OpenViduController@index'); Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom'); Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig'); // FIXME: I'm not sure about this one, should we use DELETE request maybe? Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection'); Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection'); Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest'); Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest'); } ); // Note: In Laravel 7.x we could just use withoutMiddleware() instead of a separate group Route::group( [ 'domain' => \config('app.domain'), 'prefix' => $prefix . 'api/v4' ], function () { Route::post('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom'); Route::post('openvidu/rooms/{id}/connections', 'API\V4\OpenViduController@createConnection'); // FIXME: I'm not sure about this one, should we use DELETE request maybe? Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection'); Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection'); Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest'); Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest'); } ); Route::group( [ 'domain' => \config('app.domain'), 'middleware' => 'api', 'prefix' => $prefix . 'api/v4' ], function ($router) { Route::post('support/request', 'API\V4\SupportController@request'); } ); Route::group( [ 'domain' => \config('app.domain'), 'prefix' => $prefix . 'api/webhooks' ], function () { Route::post('payment/{provider}', 'API\V4\PaymentsController@webhook'); Route::post('meet/openvidu', 'API\V4\OpenViduController@webhook'); } ); Route::group( [ 'domain' => 'services.' . \config('app.domain'), - 'prefix' => $prefix . 'api/webhooks' + 'prefix' => $prefix . 'api/webhooks/policy' ], function () { Route::post('greylist', 'API\V4\PolicyController@greylist'); + Route::post('ratelimit', 'API\V4\PolicyController@ratelimit'); Route::post('spf', 'API\V4\PolicyController@senderPolicyFramework'); } ); Route::group( [ 'domain' => 'admin.' . \config('app.domain'), 'middleware' => ['auth:api', 'admin'], 'prefix' => $prefix . 'api/v4', ], function () { Route::apiResource('domains', API\V4\Admin\DomainsController::class); Route::get('domains/{id}/confirm', 'API\V4\Admin\DomainsController@confirm'); Route::post('domains/{id}/suspend', 'API\V4\Admin\DomainsController@suspend'); Route::post('domains/{id}/unsuspend', 'API\V4\Admin\DomainsController@unsuspend'); Route::apiResource('entitlements', API\V4\Admin\EntitlementsController::class); Route::apiResource('packages', API\V4\Admin\PackagesController::class); Route::apiResource('skus', API\V4\Admin\SkusController::class); Route::apiResource('users', API\V4\Admin\UsersController::class); Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA'); Route::get('users/{id}/skus', 'API\V4\Admin\SkusController@userSkus'); Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend'); Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend'); Route::apiResource('wallets', API\V4\Admin\WalletsController::class); Route::post('wallets/{id}/one-off', 'API\V4\Admin\WalletsController@oneOff'); Route::get('wallets/{id}/transactions', 'API\V4\Admin\WalletsController@transactions'); Route::apiResource('discounts', API\V4\Admin\DiscountsController::class); Route::get('stats/chart/{chart}', 'API\V4\Admin\StatsController@chart'); } ); diff --git a/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php b/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php index aed85bd6..7462dc33 100644 --- a/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php +++ b/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php @@ -1,306 +1,312 @@ useServicesUrl(); + } + + public function tearDown() + { + $this->useNormalUrl(); + parent::tearDown(); + } + public function testSenderFailv4() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-fail.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderFailv6() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-fail.kolab.org', 'client_name' => 'mx.kolabnow.com', // actually IN AAAA gmail.com. 'client_address' => '2a00:1450:400a:801::2005', 'recipient' => $this->domainOwner->email ]; $this->assertFalse(strpos(':', $data['client_address'])); - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderNone() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-none.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderNoNet() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-none.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '256.0.0.1', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderPass() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-pass.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderPassAll() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-passall.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderPermerror() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-permerror.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderSoftfail() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-fail.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderTemperror() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-temperror.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderRelayPolicyHeloExactNegative() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['the.only.acceptable.helo'])); - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyHeloExactPositive() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['helo.some.relayservice.domain'])); - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyRegexpNegative() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['/a\.domain/'])); - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyRegexpPositive() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['/relayservice\.domain/'])); - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyWildcardSubdomainNegative() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['.helo.some.relayservice.domain'])); - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyWildcardSubdomainPositive() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['.some.relayservice.domain'])); - $response = $this->post('/api/webhooks/spf', $data); + $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); $this->domainOwner->removeSetting('spf_whitelist'); } } diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php index 47990f39..dbac8186 100644 --- a/src/tests/TestCase.php +++ b/src/tests/TestCase.php @@ -1,23 +1,43 @@ str_replace('//', '//admin.', \config('app.url'))]); url()->forceRootUrl(config('app.url')); } + + /** + * Set baseURL to the normal location + */ + protected static function useServicesUrl(): void + { + // This will set base URL for all tests in a file. + \config(['app.url' => 'https://' . \config('app.domain'))]); + url()->forceRootUrl(config('app.url')); + } + + /** + * Set baseURL to the services location + */ + protected static function useServicesUrl(): void + { + // This will set base URL for all tests in a file. + \config(['app.url' => str_replace('//', '//services.', \config('app.url'))]); + url()->forceRootUrl(config('app.url')); + } }