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
17 changes: 17 additions & 0 deletions app/Events/User/Deleting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Events\User;

use App\Events\Event;
use App\Models\User;
use Illuminate\Queue\SerializesModels;

class Deleting extends Event
{
use SerializesModels;

/**
* Create a new event instance.
*/
public function __construct(public User $user) {}
}
13 changes: 13 additions & 0 deletions app/Events/User/PasswordChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Events\User;

use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;

final class PasswordChanged
{
use Dispatchable;

public function __construct(public readonly User $user) {}
}
31 changes: 25 additions & 6 deletions app/Http/Controllers/Api/Client/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Throwable;

class AccountController extends ClientApiController
{
/**
* The number of seconds that must elapse before the email change throttle resets.
*/
private const EMAIL_UPDATE_THROTTLE = 60 * 60 * 24;

/**
* AccountController constructor.
*/
Expand Down Expand Up @@ -63,10 +70,22 @@ public function updateUsername(UpdateUsernameRequest $request): JsonResponse
*/
public function updateEmail(UpdateEmailRequest $request): JsonResponse
{
$original = $request->user()->email;
$this->updateService->handle($request->user(), $request->validated());
$user = $request->user();

// Only allow a user to change their email three times in the span
// of 24 hours. This prevents malicious users from trying to find
// existing accounts in the system by constantly changing their email.
if (RateLimiter::tooManyAttempts($key = "user:update-email:{$user->uuid}", 3)) {
throw new TooManyRequestsHttpException(message: 'Your email address has been changed too many times today. Please try again later.');
}

$original = $user->email;

if (mb_strtolower($original) !== mb_strtolower($request->validated('email'))) {
RateLimiter::hit($key, self::EMAIL_UPDATE_THROTTLE);

$this->updateService->handle($user, $request->validated());

if ($original !== $request->input('email')) {
Activity::event('user:account.email-changed')
->property(['old' => $original, 'new' => $request->input('email')])
->log();
Expand All @@ -85,7 +104,9 @@ public function updateEmail(UpdateEmailRequest $request): JsonResponse
*/
public function updatePassword(UpdatePasswordRequest $request): JsonResponse
{
$user = $this->updateService->handle($request->user(), $request->validated());
$user = Activity::event('user:account.password-changed')->transaction(function () use ($request) {
return $this->updateService->handle($request->user(), $request->validated());
});

$guard = $this->manager->guard();
// If you do not update the user in the session you'll end up working with a
Expand All @@ -98,8 +119,6 @@ public function updatePassword(UpdatePasswordRequest $request): JsonResponse
$guard->logoutOtherDevices($request->input('password'));
}

Activity::event('user:account.password-changed')->log();

return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function store(StoreBackupRequest $request, Server $server): array
}

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

$backup = $action->handle($server, $request->input('name'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function index(GetDatabasesRequest $request, Server $server): array
public function store(StoreDatabaseRequest $request, Server $server): array
{
$database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
$server->databases()->lockForUpdate();
$server->databases()->lockForUpdate()->count();

$database = $this->deployDatabaseService->handle($server, $request->validated());

Expand All @@ -87,15 +87,12 @@ public function store(StoreDatabaseRequest $request, Server $server): array
*/
public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
{
$this->managementService->rotatePassword($database);
$database->refresh();

Activity::event('server:database.rotate-password')
->subject($database)
->property('name', $database->database)
->log();
->transaction(fn () => $this->managementService->rotatePassword($database));

return $this->fractal->item($database)
return $this->fractal->item($database->refresh())
->parseIncludes(['password'])
->transformWith($this->getTransformer(DatabaseTransformer::class))
->toArray();
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Controllers/Api/Client/Servers/StartupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ public function update(UpdateStartupVariableRequest $request, Server $server): a

$startup = $this->startupCommandService->handle($server);

if ($variable->env_variable !== $request->input('value')) {
if ($original !== $request->input('value')) {
Activity::event('server:startup.edit')
->subject($variable)
->property([
'variable' => $variable->env_variable,
'old' => $original,
'new' => $request->input('value'),
'new' => $request->input('value') ?? '',
])
->log();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function __invoke(Request $request, string $backup): JsonResponse
/** @var Server $server */
$server = $model->server;
if ($server->node_id !== $node->id) {
throw new HttpForbiddenException('You do not have permission to access that backup.');
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

// Prevent backups that have already been completed from trying to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function index(ReportBackupCompleteRequest $request, string $backup): Jso
/** @var Server $server */
$server = $model->server;
if ($server->node_id !== $node->id) {
throw new HttpForbiddenException('You do not have permission to access that backup.');
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

if ($model->is_successful) {
Expand Down Expand Up @@ -97,6 +97,11 @@ public function restore(Request $request, string $backup): JsonResponse
/** @var Backup $model */
$model = Backup::query()->where('uuid', $backup)->firstOrFail();

$node = $request->attributes->get('node');
if (!$model->server->node->is($node)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

$model->server->update(['status' => null]);

Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
namespace App\Http\Controllers\Api\Remote\Servers;

use App\Enums\ContainerStatus;
use App\Exceptions\Http\HttpForbiddenException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ServerContainersController extends Controller
{
/**
* Updates the server container's status on the Panel
*/
public function status(ServerRequest $request, Server $server): JsonResponse
public function status(Request $request, Server $server): JsonResponse
{
if (!$server->node->is($request->attributes->get('node'))) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

$status = ContainerStatus::tryFrom($request->json('data.new_state')) ?? ContainerStatus::Missing;

cache()->put("servers.$server->uuid.status", $status, now()->addHour());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace App\Http\Controllers\Api\Remote\Servers;

use App\Enums\ServerState;
use App\Exceptions\Http\HttpForbiddenException;
use App\Facades\Activity;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Http\Resources\Daemon\ServerConfigurationCollection;
use App\Models\ActivityLog;
use App\Models\Backup;
Expand All @@ -17,6 +17,7 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Throwable;
use Webmozart\Assert\Assert;

class ServerDetailsController extends Controller
{
Expand All @@ -33,8 +34,21 @@ public function __construct(
* Returns details about the server that allows daemon to self-recover and ensure
* that the state of the server matches the Panel at all times.
*/
public function __invoke(ServerRequest $request, Server $server): JsonResponse
public function __invoke(Request $request, Server $server): JsonResponse
{
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);

$transfer = $server->transfer;

// If the server is being transferred allow either node to request information about
// the server. If the server is not being transferred only the target node is allowed
// to fetch these details.
$valid = $transfer ? $node->id === $transfer->old_node || $node->id === $transfer->new_node : $node->id === $server->node_id;

if (!$valid) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

return new JsonResponse([
'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,32 @@

use App\Enums\ServerState;
use App\Events\Server\Installed as ServerInstalled;
use App\Exceptions\Http\HttpForbiddenException;
use App\Exceptions\Model\DataValidationException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\InstallationDataRequest;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class ServerInstallController extends Controller
{
/**
* Returns installation information for a server.
*/
public function index(ServerRequest $request, Server $server): JsonResponse
public function index(Request $request, Server $server): JsonResponse
{
if (!$server->node->is($request->attributes->get('node'))) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

$egg = $server->egg;

return new JsonResponse([
'container_image' => $server->egg->copy_script_container,
'entrypoint' => $server->egg->copy_script_entry,
'script' => $server->egg->copy_script_install,
'container_image' => $egg->copy_script_container,
'entrypoint' => $egg->copy_script_entry,
'script' => $egg->copy_script_install,
]);
}

Expand All @@ -35,6 +42,10 @@ public function store(InstallationDataRequest $request, Server $server): JsonRes
{
$status = null;

if (!$server->node->is($request->attributes->get('node'))) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

$successful = $request->boolean('successful');

// Make sure the type of failure is accurate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@

namespace App\Http\Controllers\Api\Remote\Servers;

use App\Exceptions\Http\HttpForbiddenException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\Allocation;
use App\Models\Node;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Throwable;
use Webmozart\Assert\Assert;

class ServerTransferController extends Controller
{
Expand All @@ -29,13 +32,22 @@ public function __construct(
*
* @throws Throwable
*/
public function failure(ServerRequest $request, Server $server): JsonResponse
public function failure(Request $request, Server $server): JsonResponse
{
$transfer = $server->transfer;
if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.');
}

/* @var Node $node */
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);

// Either node can tell the panel that the transfer has failed. Only the new node
// can tell the panel that it was successful.
if (!$node->is($transfer->newNode) && !$node->is($transfer->oldNode)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

$this->connection->transaction(function () use ($transfer) {
$transfer->forceFill(['successful' => false])->saveOrFail();

Expand All @@ -53,13 +65,22 @@ public function failure(ServerRequest $request, Server $server): JsonResponse
*
* @throws Throwable
*/
public function success(ServerRequest $request, Server $server): JsonResponse
public function success(Request $request, Server $server): JsonResponse
{
$transfer = $server->transfer;
if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.');
}

/* @var Node $node */
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);

// Only the new node communicates a successful state to the panel, so we should
// not allow the old node to hit this endpoint.
if (!$node->is($transfer->newNode)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

/** @var Server $server */
$server = $this->connection->transaction(function () use ($server, $transfer) {
$data = [];
Expand Down
Loading
Loading