Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F257143
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
37 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/docker/webapp/Dockerfile b/docker/webapp/Dockerfile
index 7f142b82..cfbd755a 100755
--- a/docker/webapp/Dockerfile
+++ b/docker/webapp/Dockerfile
@@ -1,17 +1,17 @@
FROM apheleia/swoole:latest
MAINTAINER Jeroen van Meeuwen <vanmeeuwen@apheleia-it.ch>
USER root
-RUN dnf -y install findutils gnupg2 git rsync procps-ng
+RUN dnf -y install findutils gnupg2 git rsync procps-ng php-sodium
EXPOSE 8000
ARG GIT_REF=master
COPY build.sh /build.sh
RUN /build.sh
COPY init.sh /init.sh
COPY update.sh /update.sh
CMD [ "/init.sh" ]
diff --git a/src/app/Http/Controllers/API/V4/VPNController.php b/src/app/Http/Controllers/API/V4/VPNController.php
new file mode 100644
index 00000000..f8ee8d2e
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/VPNController.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\Http\Controllers\Controller;
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Lcobucci\JWT\Encoding\ChainedFormatter;
+use Lcobucci\JWT\Encoding\JoseEncoder;
+use Lcobucci\JWT\Signer\Key\InMemory;
+use Lcobucci\JWT\Signer\Rsa;
+use Lcobucci\JWT\Token\Builder;
+
+class VPNController extends Controller
+{
+ /**
+ * Token request from the vpn module
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function token(Request $request)
+ {
+ $signingKey = \config("app.vpn.token_signing_key");
+ if (empty($signingKey)) {
+ throw new \Exception("app.vpn.token_signing_key is not set");
+ }
+
+ $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
+ $token = $tokenBuilder
+ ->issuedAt(Carbon::now()->toImmutable())
+ // The entitlement is hardcoded for now to default.
+ // Can be extended in the future based on user entitlements.
+ ->withClaim('entitlement', "default")
+ ->getToken(new Rsa\Sha256(), InMemory::plainText($signingKey));
+
+ return response()->json(['status' => 'ok', 'token' => $token->toString()]);
+ }
+}
diff --git a/src/composer.json b/src/composer.json
index 47448f29..2bef1fe2 100644
--- a/src/composer.json
+++ b/src/composer.json
@@ -1,86 +1,87 @@
{
"name": "kolab/kolab4",
"type": "project",
"description": "Kolab 4",
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"repositories": [
{
"type": "vcs",
"url": "https://git.kolab.org/diffusion/PNL/php-net_ldap3.git"
}
],
"require": {
"php": "^8.1",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-dompdf": "^2.0.0",
"doctrine/dbal": "^3.3.2",
"dyrynda/laravel-nullable-fields": "^4.2.0",
"guzzlehttp/guzzle": "^7.4.1",
"kolab/net_ldap3": "dev-master",
"laravel/framework": "^9.2",
"laravel/horizon": "^5.9",
"laravel/octane": "^1.2",
"laravel/passport": "^11.3",
"laravel/tinker": "^2.7",
"league/flysystem-aws-s3-v3": "^3.0",
"mlocati/spf-lib": "^3.1",
"mollie/laravel-mollie": "^2.19",
"pear/crypt_gpg": "^1.6.6",
"predis/predis": "^2.0",
"sabre/vobject": "^4.5",
"spatie/laravel-translatable": "^6.3",
"spomky-labs/otphp": "~10.0.0",
- "stripe/stripe-php": "^10.7"
+ "stripe/stripe-php": "^10.7",
+ "lcobucci/jwt": "^5.0"
},
"require-dev": {
"code-lts/doctum": "^5.5.1",
"laravel/dusk": "~7.5.0",
"mockery/mockery": "^1.5",
"nunomaduro/larastan": "^2.0",
"phpstan/phpstan": "^1.4",
"phpunit/phpunit": "^9",
"squizlabs/php_codesniffer": "^3.6"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"database/seeds",
"include"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "stable",
"prefer-stable": true,
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
}
}
diff --git a/src/config/app.php b/src/config/app.php
index 7ae1aad5..888a3a53 100644
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -1,284 +1,288 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to place the application's name in a notification or
| any other location as required by the application or its packages.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
*/
'url' => env('APP_URL', 'http://localhost'),
'passphrase' => env('APP_PASSPHRASE', null),
'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')),
'asset_url' => env('ASSET_URL'),
'support_url' => env('SUPPORT_URL', null),
'support_email' => env('SUPPORT_EMAIL', null),
'webmail_url' => env('WEBMAIL_URL', null),
'theme' => env('APP_THEME', 'default'),
'tenant_id' => env('APP_TENANT_ID', null),
'currency' => \strtoupper(env('APP_CURRENCY', 'CHF')),
/*
|--------------------------------------------------------------------------
| Application Domain
|--------------------------------------------------------------------------
|
| System domain used for user signup (kolab identity)
*/
'domain' => env('APP_DOMAIN', 'domain.tld'),
'website_domain' => env('APP_WEBSITE_DOMAIN', env('APP_DOMAIN', 'domain.tld')),
'services_domain' => env(
'APP_SERVICES_DOMAIN',
"services." . env('APP_WEBSITE_DOMAIN', env('APP_DOMAIN', 'domain.tld'))
),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => env('APP_LOCALE', 'en'),
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\PassportServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => \Illuminate\Support\Facades\Facade::defaultAliases()->toArray(),
'headers' => [
'csp' => env('APP_HEADER_CSP', ""),
'xfo' => env('APP_HEADER_XFO', ""),
],
// Locations of knowledge base articles
'kb' => [
// An article about suspended accounts
'account_suspended' => env('KB_ACCOUNT_SUSPENDED'),
// An article about a way to delete an owned account
'account_delete' => env('KB_ACCOUNT_DELETE'),
// An article about the payment system
'payment_system' => env('KB_PAYMENT_SYSTEM'),
],
'company' => [
'name' => env('COMPANY_NAME'),
'address' => env('COMPANY_ADDRESS'),
'details' => env('COMPANY_DETAILS'),
'email' => env('COMPANY_EMAIL'),
'logo' => env('COMPANY_LOGO'),
'footer' => env('COMPANY_FOOTER', env('COMPANY_DETAILS')),
'copyright' => 'Apheleia IT AG',
],
'storage' => [
'min_qty' => (int) env('STORAGE_MIN_QTY', 5), // in GB
],
'vat' => [
'mode' => (int) env('VAT_MODE', 0),
],
'password_policy' => env('PASSWORD_POLICY') ?: 'min:6,max:255',
'payment' => [
'methods_oneoff' => env('PAYMENT_METHODS_ONEOFF', 'creditcard,paypal,banktransfer,bitcoin'),
'methods_recurring' => env('PAYMENT_METHODS_RECURRING', 'creditcard'),
],
'with_ldap' => (bool) env('APP_LDAP', true),
'with_imap' => (bool) env('APP_IMAP', false),
'with_admin' => (bool) env('APP_WITH_ADMIN', false),
'with_files' => (bool) env('APP_WITH_FILES', false),
'with_reseller' => (bool) env('APP_WITH_RESELLER', false),
'with_services' => (bool) env('APP_WITH_SERVICES', false),
'with_signup' => (bool) env('APP_WITH_SIGNUP', true),
'with_subscriptions' => (bool) env('APP_WITH_SUBSCRIPTIONS', true),
'with_wallet' => (bool) env('APP_WITH_WALLET', true),
'signup' => [
'email_limit' => (int) env('SIGNUP_LIMIT_EMAIL', 0),
'ip_limit' => (int) env('SIGNUP_LIMIT_IP', 0),
],
'woat_ns1' => env('WOAT_NS1', 'ns01.' . env('APP_DOMAIN')),
'woat_ns2' => env('WOAT_NS2', 'ns02.' . env('APP_DOMAIN')),
'ratelimit_whitelist' => explode(',', env('RATELIMIT_WHITELIST', '')),
'companion_download_link' => env(
'COMPANION_DOWNLOAD_LINK',
"https://mirror.apheleia-it.ch/pub/companion-app-beta.apk"
- )
+ ),
+
+ 'vpn' => [
+ 'signing_key' => env('VPN_TOKEN_SIGNING_KEY', 0),
+ ],
];
diff --git a/src/routes/api.php b/src/routes/api.php
index 1d581198..6aeb6cb5 100644
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -1,315 +1,317 @@
<?php
use App\Http\Controllers\API;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::group(
[
'middleware' => 'api',
'prefix' => 'auth'
],
function () {
Route::post('login', [API\AuthController::class, 'login']);
Route::group(
['middleware' => 'auth:api'],
function () {
Route::get('info', [API\AuthController::class, 'info']);
Route::post('info', [API\AuthController::class, 'info']);
Route::get('location', [API\AuthController::class, 'location']);
Route::post('logout', [API\AuthController::class, 'logout']);
Route::post('refresh', [API\AuthController::class, 'refresh']);
}
);
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => 'api',
'prefix' => 'auth'
],
function () {
Route::post('password-policy/check', [API\PasswordPolicyController::class, 'check']);
Route::post('password-reset/init', [API\PasswordResetController::class, 'init']);
Route::post('password-reset/verify', [API\PasswordResetController::class, 'verify']);
Route::post('password-reset', [API\PasswordResetController::class, 'reset']);
}
);
if (\config('app.with_signup')) {
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => 'api',
'prefix' => 'auth'
],
function () {
Route::get('signup/domains', [API\SignupController::class, 'domains']);
Route::post('signup/init', [API\SignupController::class, 'init']);
Route::get('signup/invitations/{id}', [API\SignupController::class, 'invitation']);
Route::get('signup/plans', [API\SignupController::class, 'plans']);
Route::post('signup/validate', [API\SignupController::class, 'signupValidate']);
Route::post('signup/verify', [API\SignupController::class, 'verify']);
Route::post('signup', [API\SignupController::class, 'signup']);
}
);
}
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => ['auth:api', 'scope:mfa,api'],
'prefix' => 'v4'
],
function () {
Route::post('auth-attempts/{id}/confirm', [API\V4\AuthAttemptsController::class, 'confirm']);
Route::post('auth-attempts/{id}/deny', [API\V4\AuthAttemptsController::class, 'deny']);
Route::get('auth-attempts/{id}/details', [API\V4\AuthAttemptsController::class, 'details']);
Route::get('auth-attempts', [API\V4\AuthAttemptsController::class, 'index']);
Route::post('companion/register', [API\V4\CompanionAppsController::class, 'register']);
}
);
if (\config('app.with_files')) {
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => ['auth:api', 'scope:fs,api'],
'prefix' => 'v4'
],
function () {
Route::apiResource('fs', API\V4\FsController::class);
Route::get('fs/{itemId}/permissions', [API\V4\FsController::class, 'getPermissions']);
Route::post('fs/{itemId}/permissions', [API\V4\FsController::class, 'createPermission']);
Route::put('fs/{itemId}/permissions/{id}', [API\V4\FsController::class, 'updatePermission']);
Route::delete('fs/{itemId}/permissions/{id}', [API\V4\FsController::class, 'deletePermission']);
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => [],
'prefix' => 'v4'
],
function () {
Route::post('fs/uploads/{id}', [API\V4\FsController::class, 'upload'])
->middleware(['api']);
Route::get('fs/downloads/{id}', [API\V4\FsController::class, 'download']);
}
);
}
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => ['auth:api', 'scope:api'],
'prefix' => 'v4'
],
function () {
Route::apiResource('companions', API\V4\CompanionAppsController::class);
// This must not be accessible with the 2fa token,
// to prevent an attacker from pairing a new device with a stolen token.
Route::get('companions/{id}/pairing', [API\V4\CompanionAppsController::class, 'pairing']);
Route::apiResource('domains', API\V4\DomainsController::class);
Route::get('domains/{id}/confirm', [API\V4\DomainsController::class, 'confirm']);
Route::get('domains/{id}/skus', [API\V4\DomainsController::class, 'skus']);
Route::get('domains/{id}/status', [API\V4\DomainsController::class, 'status']);
Route::post('domains/{id}/config', [API\V4\DomainsController::class, 'setConfig']);
Route::apiResource('groups', API\V4\GroupsController::class);
Route::get('groups/{id}/skus', [API\V4\GroupsController::class, 'skus']);
Route::get('groups/{id}/status', [API\V4\GroupsController::class, 'status']);
Route::post('groups/{id}/config', [API\V4\GroupsController::class, 'setConfig']);
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('rooms', API\V4\RoomsController::class);
Route::post('rooms/{id}/config', [API\V4\RoomsController::class, 'setConfig']);
Route::get('rooms/{id}/skus', [API\V4\RoomsController::class, 'skus']);
Route::post('meet/rooms/{id}', [API\V4\MeetController::class, 'joinRoom'])
->withoutMiddleware(['auth:api', 'scope:api']);
Route::apiResource('resources', API\V4\ResourcesController::class);
Route::get('resources/{id}/skus', [API\V4\ResourcesController::class, 'skus']);
Route::get('resources/{id}/status', [API\V4\ResourcesController::class, 'status']);
Route::post('resources/{id}/config', [API\V4\ResourcesController::class, 'setConfig']);
Route::apiResource('shared-folders', API\V4\SharedFoldersController::class);
Route::get('shared-folders/{id}/skus', [API\V4\SharedFoldersController::class, 'skus']);
Route::get('shared-folders/{id}/status', [API\V4\SharedFoldersController::class, 'status']);
Route::post('shared-folders/{id}/config', [API\V4\SharedFoldersController::class, 'setConfig']);
Route::apiResource('skus', API\V4\SkusController::class);
Route::apiResource('users', API\V4\UsersController::class);
Route::post('users/{id}/config', [API\V4\UsersController::class, 'setConfig']);
Route::get('users/{id}/skus', [API\V4\UsersController::class, 'skus']);
Route::get('users/{id}/status', [API\V4\UsersController::class, 'status']);
Route::apiResource('wallets', API\V4\WalletsController::class);
Route::get('wallets/{id}/transactions', [API\V4\WalletsController::class, 'transactions']);
Route::get('wallets/{id}/receipts', [API\V4\WalletsController::class, 'receipts']);
Route::get('wallets/{id}/receipts/{receipt}', [API\V4\WalletsController::class, 'receiptDownload']);
Route::get('password-policy', [API\PasswordPolicyController::class, 'index']);
Route::post('password-reset/code', [API\PasswordResetController::class, 'codeCreate']);
Route::delete('password-reset/code/{id}', [API\PasswordResetController::class, 'codeDelete']);
Route::post('payments', [API\V4\PaymentsController::class, 'store']);
//Route::delete('payments', [API\V4\PaymentsController::class, 'cancel']);
Route::get('payments/mandate', [API\V4\PaymentsController::class, 'mandate']);
Route::post('payments/mandate', [API\V4\PaymentsController::class, 'mandateCreate']);
Route::put('payments/mandate', [API\V4\PaymentsController::class, 'mandateUpdate']);
Route::delete('payments/mandate', [API\V4\PaymentsController::class, 'mandateDelete']);
Route::post('payments/mandate/reset', [API\V4\PaymentsController::class, 'mandateReset']);
Route::get('payments/methods', [API\V4\PaymentsController::class, 'paymentMethods']);
Route::get('payments/pending', [API\V4\PaymentsController::class, 'payments']);
Route::get('payments/has-pending', [API\V4\PaymentsController::class, 'hasPayments']);
Route::get('payments/status', [API\V4\PaymentsController::class, 'paymentStatus']);
Route::post('support/request', [API\V4\SupportController::class, 'request'])
->withoutMiddleware(['auth:api', 'scope:api'])
->middleware(['api']);
+
+ Route::get('vpn/token', [API\V4\VPNController::class, 'token']);
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'prefix' => 'webhooks'
],
function () {
Route::post('payment/{provider}', [API\V4\PaymentsController::class, 'webhook']);
Route::post('meet', [API\V4\MeetController::class, 'webhook']);
}
);
if (\config('app.with_services')) {
Route::group(
[
'domain' => \config('app.services_domain'),
'prefix' => 'webhooks'
],
function () {
Route::get('nginx', [API\V4\NGINXController::class, 'authenticate']);
Route::get('nginx-roundcube', [API\V4\NGINXController::class, 'authenticateRoundcube']);
Route::get('nginx-httpauth', [API\V4\NGINXController::class, 'httpauth']);
Route::post('cyrus-sasl', [API\V4\NGINXController::class, 'cyrussasl']);
Route::post('policy/greylist', [API\V4\PolicyController::class, 'greylist']);
Route::post('policy/ratelimit', [API\V4\PolicyController::class, 'ratelimit']);
Route::post('policy/spf', [API\V4\PolicyController::class, 'senderPolicyFramework']);
}
);
}
if (\config('app.with_admin')) {
Route::group(
[
'domain' => 'admin.' . \config('app.website_domain'),
'middleware' => ['auth:api', 'admin'],
'prefix' => 'v4',
],
function () {
Route::apiResource('domains', API\V4\Admin\DomainsController::class);
Route::get('domains/{id}/skus', [API\V4\Admin\DomainsController::class, 'skus']);
Route::post('domains/{id}/suspend', [API\V4\Admin\DomainsController::class, 'suspend']);
Route::post('domains/{id}/unsuspend', [API\V4\Admin\DomainsController::class, 'unsuspend']);
Route::apiResource('groups', API\V4\Admin\GroupsController::class);
Route::post('groups/{id}/suspend', [API\V4\Admin\GroupsController::class, 'suspend']);
Route::post('groups/{id}/unsuspend', [API\V4\Admin\GroupsController::class, 'unsuspend']);
Route::apiResource('resources', API\V4\Admin\ResourcesController::class);
Route::apiResource('shared-folders', API\V4\Admin\SharedFoldersController::class);
Route::apiResource('skus', API\V4\Admin\SkusController::class);
Route::apiResource('users', API\V4\Admin\UsersController::class);
Route::get('users/{id}/discounts', [API\V4\Reseller\DiscountsController::class, 'userDiscounts']);
Route::post('users/{id}/reset2FA', [API\V4\Admin\UsersController::class, 'reset2FA']);
Route::post('users/{id}/resetGeoLock', [API\V4\Admin\UsersController::class, 'resetGeoLock']);
Route::post('users/{id}/resync', [API\V4\Admin\UsersController::class, 'resync']);
Route::get('users/{id}/skus', [API\V4\Admin\UsersController::class, 'skus']);
Route::post('users/{id}/skus/{sku}', [API\V4\Admin\UsersController::class, 'setSku']);
Route::post('users/{id}/suspend', [API\V4\Admin\UsersController::class, 'suspend']);
Route::post('users/{id}/unsuspend', [API\V4\Admin\UsersController::class, 'unsuspend']);
Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
Route::post('wallets/{id}/one-off', [API\V4\Admin\WalletsController::class, 'oneOff']);
Route::get('wallets/{id}/transactions', [API\V4\Admin\WalletsController::class, 'transactions']);
Route::get('stats/chart/{chart}', [API\V4\Admin\StatsController::class, 'chart']);
}
);
}
if (\config('app.with_reseller')) {
Route::group(
[
'domain' => 'reseller.' . \config('app.website_domain'),
'middleware' => ['auth:api', 'reseller'],
'prefix' => 'v4',
],
function () {
Route::apiResource('domains', API\V4\Reseller\DomainsController::class);
Route::get('domains/{id}/skus', [API\V4\Reseller\DomainsController::class, 'skus']);
Route::post('domains/{id}/suspend', [API\V4\Reseller\DomainsController::class, 'suspend']);
Route::post('domains/{id}/unsuspend', [API\V4\Reseller\DomainsController::class, 'unsuspend']);
Route::apiResource('groups', API\V4\Reseller\GroupsController::class);
Route::post('groups/{id}/suspend', [API\V4\Reseller\GroupsController::class, 'suspend']);
Route::post('groups/{id}/unsuspend', [API\V4\Reseller\GroupsController::class, 'unsuspend']);
Route::apiResource('invitations', API\V4\Reseller\InvitationsController::class);
Route::post('invitations/{id}/resend', [API\V4\Reseller\InvitationsController::class, 'resend']);
Route::post('payments', [API\V4\Reseller\PaymentsController::class, 'store']);
Route::get('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandate']);
Route::post('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateCreate']);
Route::put('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateUpdate']);
Route::delete('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateDelete']);
Route::get('payments/methods', [API\V4\Reseller\PaymentsController::class, 'paymentMethods']);
Route::get('payments/pending', [API\V4\Reseller\PaymentsController::class, 'payments']);
Route::get('payments/has-pending', [API\V4\Reseller\PaymentsController::class, 'hasPayments']);
Route::apiResource('resources', API\V4\Reseller\ResourcesController::class);
Route::apiResource('shared-folders', API\V4\Reseller\SharedFoldersController::class);
Route::apiResource('skus', API\V4\Reseller\SkusController::class);
Route::apiResource('users', API\V4\Reseller\UsersController::class);
Route::get('users/{id}/discounts', [API\V4\Reseller\DiscountsController::class, 'userDiscounts']);
Route::post('users/{id}/reset2FA', [API\V4\Reseller\UsersController::class, 'reset2FA']);
Route::post('users/{id}/resetGeoLock', [API\V4\Reseller\UsersController::class, 'resetGeoLock']);
Route::post('users/{id}/resync', [API\V4\Reseller\UsersController::class, 'resync']);
Route::get('users/{id}/skus', [API\V4\Reseller\UsersController::class, 'skus']);
Route::post('users/{id}/skus/{sku}', [API\V4\Admin\UsersController::class, 'setSku']);
Route::post('users/{id}/suspend', [API\V4\Reseller\UsersController::class, 'suspend']);
Route::post('users/{id}/unsuspend', [API\V4\Reseller\UsersController::class, 'unsuspend']);
Route::apiResource('wallets', API\V4\Reseller\WalletsController::class);
Route::post('wallets/{id}/one-off', [API\V4\Reseller\WalletsController::class, 'oneOff']);
Route::get('wallets/{id}/receipts', [API\V4\Reseller\WalletsController::class, 'receipts']);
Route::get('wallets/{id}/receipts/{receipt}', [API\V4\Reseller\WalletsController::class, 'receiptDownload']);
Route::get('wallets/{id}/transactions', [API\V4\Reseller\WalletsController::class, 'transactions']);
Route::get('stats/chart/{chart}', [API\V4\Reseller\StatsController::class, 'chart']);
}
);
}
diff --git a/src/tests/Feature/Controller/VPNTest.php b/src/tests/Feature/Controller/VPNTest.php
new file mode 100644
index 00000000..10b67a7c
--- /dev/null
+++ b/src/tests/Feature/Controller/VPNTest.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use Tests\TestCase;
+use Carbon\Carbon;
+use Lcobucci\JWT\Encoding\JoseEncoder;
+use Lcobucci\JWT\Token\Parser;
+use Lcobucci\JWT\Token\RegisteredClaims;
+use Lcobucci\JWT\Signer\Rsa\Sha256;
+use Lcobucci\JWT\Signer\Key\InMemory;
+use Lcobucci\JWT\Validation\Constraint;
+use Lcobucci\JWT\Validation\Validator;
+use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
+
+class VPNTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ Carbon::setTestNow(Carbon::create(2022, 02, 02, 13, 0, 0));
+ parent::setUp();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ /**
+ * Test the webhook
+ */
+ public function testToken(): void
+ {
+ // openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
+ $privateKey = <<<EOF
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmfUb5J+na6LV1
+6PTmOX6alvyEbWEUA1HykKFsKRq3q8nraZU4LlfhM1qoeA23re4WEmT2SoCXq8sd
+ewRXP+swYl6vAPZFtqCR52vApEXn+b221g3gEa5KnZeQ3APTej6wcs+3etMcDFAh
+Sabrjvl3eoHXmO2RQoeSjSfp6N4LPvbbOjkLR5EYdDNxVuwU7HRkwkXve1rWh94z
+RbSl7o4XkP/qzOGEDfrm7SivcSWqwFndHkxNeum1FnBfDZ9ChOwEunuIZiPX2TDb
+K3WZ+TJn29xNERAJJgFZLspB9RK+FT/lCX4IZgdl8ZyU8lC68QXaZuAcQhg7TU7l
+xBw7ZJXBAgMBAAECggEAAqEL/R9tJmISNYJgKv+MQ9mFCNFSXQwgCpN6nRg5aLOb
+h25o0b+M7zc4pAbrTokpBJZgm5wPE6C99HakGahJKOqGF2o4OV8aLNupnFvrvQSj
+reQLnuF17g3hiMMqA02qTkPYiwud6FLlyV9zSu58etoc0UsEBg0gfMcNCDj/bVAt
+b+JvZjfFXG9TQRNx7F3H/yRO5kHw+oqeDBAvHN+Mkq9KATLQ8GBZznwpN7fHL+Is
+PZCDvcAg/4OtN1cNgMOKkdBDSMzjtn7xHUX9jl03VUPvXfa9KDdFCMdVndtnACRK
+lFNMdD+ugQ7Ch1oHI16HUWo663fjF+9m3neYjoAiwQKBgQC+UykotVMHX65OsYx7
+tXankRmj9WnoiUFrfClrdVynbkcbZvCt9NdNluv0eNRbEaNXD07nOIppACSDaQ0i
+DdIALbFU6I3kzAF+z25Rh6SaIl5gaUGxSLSzmpswJsv0hiu/5Sb8t6fOX+VWBV7I
+Fw7MqASoggV5rEAvRVtwiw4TsQKBgQDf8IwZDwXlTFc7/VsAjLfb+edpVcavOB9d
+DAXAwUH+2FJb9AWPOLrTnKdsRV9yw9FueMciuROGi9btOYxEcLGvNcvsyaWy0eFQ
+vN2w0NsxpBodRylxMQLGDc5wO9lbrVuftzC0rDaQaD9gPQaKFoQI7ww81Jmx6u1Y
+OP/Nsq53EQKBgD8vIoHmOItBI3/yh53mL18P18BLz/4n2vURAjsvejQHc0nQkeRe
+XT/f87N0jaMyJtTXOy2d4q1bI8QQkxCUH/x5Lt7uWXT0mSZ9PLWKX4XgFQ7SwsFV
+TtA1aoHAz4L9K/cH3zqUyfvEcEFvhPjOVtZwjSNYDvNG0QQgdWvWbjTxAoGAE50R
+6C/0qDyjd1GdYtLwV4fvyL4GhNo5hQDEkDlc+mEf9YXN5tllI5uY3lbFIVwdP7u8
+VUI4f5RH4scjjesA5QOlNLwEk0DmpxejoxTn3dUtpFrTOmK8h3Q2HIZhZzIr0DVP
+QsPCk6tNwbQWmomWTuIBBGLqgza8SvnTDcUUmsECgYBXk4MAk1FennJuDvpiD1Gg
+HiIQykUOQeA5wk+R97X7D4kI8kj6XzCuRjG+nSJiYmpZBHRvA5XtRtMJWep11R6O
+8yu8ftj4xBQr6roUoHwJ/JBxe8JuKW3yh52CaZLP2KjizwzNI0hDMUzinZIReex+
+iFyt8WwtMwzW/520PlwUyQ==
+-----END PRIVATE KEY-----
+EOF;
+
+ // openssl rsa -pubout -in private.pem -out public.pem
+ $publicKey = <<<EOF
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApn1G+Sfp2ui1dej05jl+
+mpb8hG1hFANR8pChbCkat6vJ62mVOC5X4TNaqHgNt63uFhJk9kqAl6vLHXsEVz/r
+MGJerwD2RbagkedrwKRF5/m9ttYN4BGuSp2XkNwD03o+sHLPt3rTHAxQIUmm6475
+d3qB15jtkUKHko0n6ejeCz722zo5C0eRGHQzcVbsFOx0ZMJF73ta1ofeM0W0pe6O
+F5D/6szhhA365u0or3ElqsBZ3R5MTXrptRZwXw2fQoTsBLp7iGYj19kw2yt1mfky
+Z9vcTREQCSYBWS7KQfUSvhU/5Ql+CGYHZfGclPJQuvEF2mbgHEIYO01O5cQcO2SV
+wQIDAQAB
+-----END PUBLIC KEY-----
+EOF;
+
+ \config(['app.vpn.token_signing_key' => $privateKey]);
+
+ $john = $this->getTestUser('john@kolab.org');
+
+ $response = $this->get("api/v4/vpn/token");
+ $response->assertStatus(401);
+
+ $response = $this->actingAs($john)->get("api/v4/vpn/token");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $jwt = $json['token'];
+
+ $parser = new Parser(new JoseEncoder());
+
+ /** @var \Lcobucci\JWT\UnencryptedToken $token */
+ $token = $parser->parse($jwt);
+
+ $this->assertSame("default", $token->claims()->get('entitlement'));
+ $this->assertSame("2022-02-02T13:00:00+00:00", $token->claims()->get(
+ RegisteredClaims::ISSUED_AT
+ )->format(\DateTimeImmutable::RFC3339));
+ $this->assertSame(0, Carbon::now()->diffInSeconds(new Carbon($token->claims()->get(
+ RegisteredClaims::ISSUED_AT
+ ))));
+
+ $validator = new Validator();
+ $key = InMemory::plainText($publicKey);
+ $validator->assert($token, new Constraint\SignedWith(new Sha256(), $key));
+
+ $invalidKey = <<<EOF
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApn1G+Sfp2ui1dej05jl+
+mpb8hG1hFANR8pChbCkat6vJ62mVOC5X4TNaqHgNt63uFhJk9kqAl6vLHXsEVz/r
+MGJerwD2RbagkedrwKRF5/m9ttYN4BGuSp2XkNwD03o+sHLPt3rTHAxQIUmm6475
+d3qB15jtkUKHko0n6ejeCz722zo5C0eRGHQzcVbsFOx0ZMJF73ta1ofeM0W0pe6O
+F5D/6szhhA365u0or3ElqsBZ3R5MTXrptRZwXw2fQoTsBLp7iGYj19kw2yt1mfky
+Z9vcTREQCSYBWS7KQfUSvhU/5Ql+CGYHZfGclPJQuvEF2mbgHEIYO01O5cQcO2SV
+wQIDAQAC
+-----END PUBLIC KEY-----
+EOF;
+ $this->expectException(RequiredConstraintsViolated::class);
+ $key = InMemory::plainText($invalidKey);
+ $validator->assert($token, new Constraint\SignedWith(new Sha256(), $key));
+ }
+}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Jun 10, 6:40 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
197187
Default Alt Text
(37 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment