Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions app/Enums/ResourceLimit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Enums;

use App\Models\Server;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\RateLimiter;
use Webmozart\Assert\Assert;

/**
* A basic resource throttler for individual servers. This is applied in addition
* to existing rate limits and allows the code to slow down speedy users that might
* be creating resources a little too quickly for comfort. This throttle generally
* only applies to creation flows, and not general view/edit/delete flows.
*/
enum ResourceLimit: string
{
case Websocket = 'websocket';
case AllocationCreate = 'allocation-create';
case BackupRestore = 'backup-restore';
case DatabaseCreate = 'database-create';
case ScheduleCreate = 'schedule-create';
case SubuserCreate = 'subuser-create';
case FilePull = 'file-pull';
Comment thread
Boy132 marked this conversation as resolved.

public function throttleKey(): string
{
return "api.client:server-resource:{$this->name}";
}

/**
* Returns a middleware that will throttle the specific resource by server. This
* throttle applies to any user making changes to that resource on the specific
* server, it is NOT per-user.
*/
public function middleware(): string
{
return ThrottleRequests::using($this->throttleKey());
}

public function limit(): Limit
{
return match ($this) {
self::Websocket => Limit::perMinute(5),
self::BackupRestore => Limit::perMinutes(15, 3),
self::DatabaseCreate => Limit::perMinute(2),
self::SubuserCreate => Limit::perMinutes(15, 10),
self::FilePull => Limit::perMinutes(10, 5),
default => Limit::perMinute(2),
};
}

public static function boot(): void
{
foreach (self::cases() as $case) {
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);

return $case->limit()->by($server->uuid);
});
}
}
}
4 changes: 2 additions & 2 deletions app/Extensions/OAuth/Schemas/GithubSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ public function getId(): string
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
Step::make('Register new GitHub OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">GitHub Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
Expand Down
4 changes: 1 addition & 3 deletions app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,11 @@ public function form(Schema $schema): Schema
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->helperText(trans('admin/node.upload_limit_help.0'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.1'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
Comment thread
Boy132 marked this conversation as resolved.
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(1)
Expand Down
6 changes: 3 additions & 3 deletions app/Filament/Admin/Resources/Nodes/Pages/EditNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,10 @@ public function form(Schema $schema): Schema
'lg' => 1,
])
->label(trans('admin/node.upload_limit'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1'))
->numeric()->required()
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help'))
->numeric()
->required()
->minValue(1)
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan([
Expand Down
18 changes: 12 additions & 6 deletions app/Http/Controllers/Api/Client/Servers/BackupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,21 @@ public function store(StoreBackupRequest $request, Server $server): array
// how best to allow a user to create a backup that is locked without also preventing
// them from just filling up a server with backups that can never be deleted?
if ($request->user()->can(SubuserPermission::BackupDelete, $server)) {
$action->setIsLocked((bool) $request->input('is_locked'));
$action->setIsLocked($request->boolean('is_locked'));
}

$backup = $action->handle($server, $request->input('name'));
$backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
$server->backups()->lockForUpdate();

Activity::event('server:backup.start')
->subject($backup)
->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
->log();
$backup = $action->handle($server, $request->input('name'));

$log->subject($backup)->property([
'name' => $backup->name,
'locked' => $request->boolean('is_locked'),
]);

return $backup;
});

return $this->fractal->item($backup)
->transformWith($this->getTransformer(BackupTransformer::class))
Expand Down
13 changes: 8 additions & 5 deletions app/Http/Controllers/Api/Client/Servers/DatabaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ public function index(GetDatabasesRequest $request, Server $server): array
*/
public function store(StoreDatabaseRequest $request, Server $server): array
{
$database = $this->deployDatabaseService->handle($server, $request->validated());
$database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
$server->databases()->lockForUpdate();

Activity::event('server:database.create')
->subject($database)
->property('name', $database->database)
->log();
$database = $this->deployDatabaseService->handle($server, $request->validated());

$log->subject($database)->property('name', $database->database);

return $database;
});
Comment thread
Boy132 marked this conversation as resolved.

return $this->fractal->item($database)
->parseIncludes(['password'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,19 @@ public function setPrimary(SetPrimaryAllocationRequest $request, Server $server,
*/
public function store(NewAllocationRequest $request, Server $server): array
{
if ($server->allocations()->count() >= $server->allocation_limit) {
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
}
$allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) {
$server->allocations()->lockForUpdate();

$allocation = $this->assignableAllocationService->handle($server);
if ($server->allocations->count() >= $server->allocation_limit) {
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
}

Activity::event('server:allocation.create')
->subject($allocation)
->property('allocation', $allocation->address)
->log();
$allocation = $this->assignableAllocationService->handle($server);

$log->subject($allocation)->property('allocation', $allocation->address);

return $allocation;
});
Comment thread
Boy132 marked this conversation as resolved.

return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public function __invoke(Request $request, string $backup): JsonResponse
*/
private function getConfiguredMaxPartSize(): int
{
$maxPartSize = (int) config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
$maxPartSize = config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
if ($maxPartSize <= 0) {
$maxPartSize = self::DEFAULT_MAX_PART_SIZE;
}
Expand Down
2 changes: 1 addition & 1 deletion app/Models/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class Node extends Model implements Validatable
'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
'daemon_connect' => ['required', 'numeric', 'between:1,65535'],
'maintenance_mode' => ['boolean'],
'upload_size' => ['int', 'between:1,1024'],
'upload_size' => ['int', 'min:1'],
'tags' => ['array'],
];

Expand Down
3 changes: 3 additions & 0 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Providers;

use App\Enums\ResourceLimit;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
Expand Down Expand Up @@ -98,5 +99,7 @@ protected function configureRateLimiting(): void
config('http.rate_limit.application')
)->by($key);
});

ResourceLimit::boot();
}
}
18 changes: 18 additions & 0 deletions app/Repositories/Daemon/DaemonServerRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ public function cancelTransfer(): void
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
* correctly and avoids any costly mistakes in the codebase.
*
* @deprecated
* @see self::deauthorize()
*
* @throws ConnectionException
*/
public function revokeUserJTI(int $id): void
Expand All @@ -143,6 +146,21 @@ public function revokeUserJTI(int $id): void
]);
}

/**
* Deauthorizes a user (disconnects websockets and SFTP) on the Wings instance for the server.
*
* @throws ConnectionException
*/
public function deauthorize(string $user): void
{
$this->getHttpClient()->post('/api/deauthorize-user', [
'json' => [
'user' => $user,
'servers' => [$this->server->uuid],
],
]);
}
Comment thread
Boy132 marked this conversation as resolved.

public function getInstallLogs(): string
{
return $this->getHttpClient()
Expand Down
2 changes: 1 addition & 1 deletion app/Services/Servers/DetailsModificationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function handle(Server $server, array $data): Server
// websockets.
if ($server->owner_id !== $owner) {
try {
$this->serverRepository->setServer($server)->revokeUserJTI($owner);
$this->serverRepository->setServer($server)->deauthorize($server->user->uuid);
} catch (ConnectionException) {
// Do nothing. A failure here is not ideal, but it is likely to be caused by daemon
// being offline, or in an entirely broken state. Remember, these tokens reset every
Expand Down
5 changes: 4 additions & 1 deletion app/Services/Servers/ServerDeletionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ public function handle(Server $server): void
}
}

$server->allocations()->update(['server_id' => null]);
$server->allocations()->update([
'server_id' => null,
'notes' => null,
]);

$server->delete();
});
Expand Down
2 changes: 1 addition & 1 deletion app/Services/Subusers/SubuserDeletionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function handle(Subuser $subuser, Server $server): void
event(new SubUserRemoved($subuser->server, $subuser->user));

try {
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance.
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
Expand Down
2 changes: 1 addition & 1 deletion app/Services/Subusers/SubuserUpdateService.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function handle(Subuser $subuser, Server $server, array $permissions): vo
$subuser->update(['permissions' => $cleanedPermissions]);

try {
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance. Chances are it is
// offline and the token will be invalid once daemon boots back.
Expand Down
4 changes: 4 additions & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class,
]);

$middleware->priority([
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
})
->withSingletons([
\Illuminate\Contracts\Console\Kernel::class => \App\Console\Kernel::class,
Expand Down
11 changes: 6 additions & 5 deletions config/backups.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Http\Controllers\Api\Remote\Backups\BackupRemoteUploadController;
use App\Models\Backup;

return [
Expand All @@ -10,25 +11,25 @@

// This value is used to determine the lifespan of UploadPart presigned urls that daemon
// uses to upload backups to S3 storage. Value is in minutes, so this would default to an hour.
'presigned_url_lifespan' => env('BACKUP_PRESIGNED_URL_LIFESPAN', 60),
'presigned_url_lifespan' => (int) env('BACKUP_PRESIGNED_URL_LIFESPAN', 60),

// This value defines the maximal size of a single part for the S3 multipart upload during backups
// The maximal part size must be given in bytes. The default value is 5GB.
// Note that 5GB is the maximum for a single part when using AWS S3.
'max_part_size' => env('BACKUP_MAX_PART_SIZE', 5 * 1024 * 1024 * 1024),
'max_part_size' => (int) env('BACKUP_MAX_PART_SIZE', BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE),

// The time to wait before automatically failing a backup, time is in minutes and defaults
// to 6 hours. To disable this feature, set the value to `0`.
'prune_age' => env('BACKUP_PRUNE_AGE', 360),
'prune_age' => (int) env('BACKUP_PRUNE_AGE', 360),

// Defines the backup creation throttle limits for users. In this default example, we allow
// a user to create two (successful or pending) backups per 10 minutes. Even if they delete
// a backup it will be included in the throttle count.
//
// Set the period to "0" to disable this throttle. The period is defined in seconds.
'throttles' => [
'limit' => env('BACKUP_THROTTLE_LIMIT', 2),
'period' => env('BACKUP_THROTTLE_PERIOD', 600),
'limit' => (int) env('BACKUP_THROTTLE_LIMIT', 2),
'period' => (int) env('BACKUP_THROTTLE_PERIOD', 600),
],

'disks' => [
Expand Down
2 changes: 1 addition & 1 deletion config/http.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
'rate_limit' => [
'client_period' => 1,
'client' => env('APP_API_CLIENT_RATELIMIT', 720),
'client' => env('APP_API_CLIENT_RATELIMIT', 120),

'application_period' => 1,
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),
Expand Down
2 changes: 1 addition & 1 deletion contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ To start contributing to Pelican Panel, you need to have a basic understanding o

* [PHP](https://php.net) & [Laravel](https://laravel.com)
* [Livewire](https://laravel-livewire.com) & [Filament](https://filamentphp.com)
* [Git](https://git-scm.com) & [Github](https://github.com)
* [Git](https://git-scm.com) & [GitHub](https://github.com)

## Dev Environment Setup

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('allocations')
->whereNull('server_id')
->update(['notes' => null]);
}
Comment thread
Boy132 marked this conversation as resolved.

/**
* Reverse the migrations.
*/
public function down(): void
{
// Not needed
}
};
5 changes: 1 addition & 4 deletions lang/en/admin/node.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,7 @@

'tags' => 'Tags',
'upload_limit' => 'Upload Limit',
'upload_limit_help' => [
'Enter the maximum size of files that can be uploaded through the web-based file manager.',
'Make sure your webserver supports file uploads of this size!',
],
'upload_limit_help' => 'Enter the maximum size of files that can be uploaded through the web-based file manager.',
Comment thread
Boy132 marked this conversation as resolved.
'sftp_port' => 'SFTP Port',
'sftp_alias' => 'SFTP Alias',
'sftp_alias_help' => 'Display alias for the SFTP address. Leave empty to use the Node FQDN.',
Expand Down
Loading