Skip to content

Commit

Permalink
Update backup logic to use activity logs, not audit logs
Browse files Browse the repository at this point in the history
  • Loading branch information
DaneEveritt committed May 29, 2022
1 parent cbecfff commit 2fc5a73
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 161 deletions.
5 changes: 2 additions & 3 deletions app/Facades/Activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
* @method static ActivityLogService anonymous()
* @method static ActivityLogService event(string $action)
* @method static ActivityLogService description(?string $description)
* @method static ActivityLogService subject(Model $subject)
* @method static ActivityLogService subject(Model|Model[] $subject)
* @method static ActivityLogService actor(Model $actor)
* @method static ActivityLogService withProperties(\Illuminate\Support\Collection|array $properties)
* @method static ActivityLogService withRequestMetadata()
* @method static ActivityLogService property(string $key, mixed $value)
* @method static ActivityLogService property(string|array $key, mixed $value = null)
* @method static \Pterodactyl\Models\ActivityLog log(string $description = null)
* @method static ActivityLogService clone()
* @method static mixed transaction(\Closure $callback)
Expand Down
63 changes: 30 additions & 33 deletions app/Http/Controllers/Api/Client/Servers/BackupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
use Illuminate\Http\Request;
use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\AuditLog;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Facades\Activity;
use Pterodactyl\Models\Permission;
use Illuminate\Auth\Access\AuthorizationException;
use Pterodactyl\Services\Backups\DeleteBackupService;
Expand Down Expand Up @@ -77,25 +77,23 @@ public function index(Request $request, Server $server): array
*/
public function store(StoreBackupRequest $request, Server $server): array
{
/** @var \Pterodactyl\Models\Backup $backup */
$backup = $server->audit(AuditLog::SERVER__BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) {
$action = $this->initiateBackupService
->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));

// Only set the lock status if the user even has permission to delete backups,
// otherwise ignore this status. This gets a little funky since it isn't clear
// 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(Permission::ACTION_BACKUP_DELETE, $server)) {
$action->setIsLocked((bool) $request->input('is_locked'));
}

$backup = $action->handle($server, $request->input('name'));
$action = $this->initiateBackupService
->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));

// Only set the lock status if the user even has permission to delete backups,
// otherwise ignore this status. This gets a little funky since it isn't clear
// 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(Permission::ACTION_BACKUP_DELETE, $server)) {
$action->setIsLocked((bool) $request->input('is_locked'));
}

$model->metadata = ['backup_uuid' => $backup->uuid];
$backup = $action->handle($server, $request->input('name'));

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

return $this->fractal->item($backup)
->transformWith($this->getTransformer(BackupTransformer::class))
Expand All @@ -114,14 +112,11 @@ public function toggleLock(Request $request, Server $server, Backup $backup): ar
throw new AuthorizationException();
}

$action = $backup->is_locked ? AuditLog::SERVER__BACKUP_UNLOCKED : AuditLog::SERVER__BACKUP_LOCKED;
$server->audit($action, function (AuditLog $audit) use ($backup) {
$audit->metadata = ['backup_uuid' => $backup->uuid];
$action = $backup->is_locked ? 'server:backup.unlock' : 'server:backup.lock';

$backup->update(['is_locked' => !$backup->is_locked]);
});
$backup->update(['is_locked' => !$backup->is_locked]);

$backup->refresh();
Activity::event($action)->subject($backup)->property('name', $backup->name)->log();

return $this->fractal->item($backup)
->transformWith($this->getTransformer(BackupTransformer::class))
Expand Down Expand Up @@ -156,11 +151,12 @@ public function delete(Request $request, Server $server, Backup $backup): JsonRe
throw new AuthorizationException();
}

$server->audit(AuditLog::SERVER__BACKUP_DELETED, function (AuditLog $audit) use ($backup) {
$audit->metadata = ['backup_uuid' => $backup->uuid];
$this->deleteBackupService->handle($backup);

$this->deleteBackupService->handle($backup);
});
Activity::event('server:backup.delete')
->subject($backup)
->property(['name' => $backup->name, 'failed' => !$backup->is_successful])
->log();

return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
Expand All @@ -184,9 +180,8 @@ public function download(Request $request, Server $server, Backup $backup): Json
}

$url = $this->downloadLinkService->handle($backup, $request->user());
$server->audit(AuditLog::SERVER__BACKUP_DOWNLOADED, function (AuditLog $audit) use ($backup) {
$audit->metadata = ['backup_uuid' => $backup->uuid];
});

Activity::event('server:backup.download')->subject($backup)->property('name', $backup->name)->log();

return new JsonResponse([
'object' => 'signed_url',
Expand Down Expand Up @@ -221,9 +216,11 @@ public function restore(Request $request, Server $server, Backup $backup): JsonR
throw new BadRequestHttpException('This backup cannot be restored at this time: not completed or failed.');
}

$server->audit(AuditLog::SERVER__BACKUP_RESTORE_STARTED, function (AuditLog $audit, Server $server) use ($backup, $request) {
$audit->metadata = ['backup_uuid' => $backup->uuid];
$log = Activity::event('server:backup.restore')
->subject($backup)
->property(['name' => $backup->name, 'truncate' => $request->input('truncate')]);

$log->transaction(function () use ($backup, $server, $request) {
// If the backup is for an S3 file we need to generate a unique Download link for
// it that will allow Wings to actually access the file.
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
Expand Down
32 changes: 12 additions & 20 deletions app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
use Carbon\CarbonImmutable;
use Illuminate\Http\Request;
use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\AuditLog;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Facades\Activity;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
Expand Down Expand Up @@ -46,15 +45,12 @@ public function index(ReportBackupCompleteRequest $request, string $backup)
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
}

$action = $request->input('successful')
? AuditLog::SERVER__BACKUP_COMPELTED
: AuditLog::SERVER__BACKUP_FAILED;

$model->server->audit($action, function (AuditLog $audit) use ($model, $request) {
$audit->is_system = true;
$audit->metadata = ['backup_uuid' => $model->uuid];
$action = $request->boolean('successful') ? 'server:backup.complete' : 'server:backup.failed';
$log = Activity::event($action)->subject($model, $model->server)->property('name', $model->name);

$log->transaction(function () use ($model, $request) {
$successful = $request->boolean('successful');

$model->fill([
'is_successful' => $successful,
// Change the lock state to unlocked if this was a failed backup so that it can be
Expand Down Expand Up @@ -93,17 +89,13 @@ public function restore(Request $request, string $backup)
{
/** @var \Pterodactyl\Models\Backup $model */
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
$action = $request->get('successful')
? AuditLog::SERVER__BACKUP_RESTORE_COMPLETED
: AuditLog::SERVER__BACKUP_RESTORE_FAILED;

// Just create a new audit entry for this event and update the server state
// so that power actions, file management, and backups can resume as normal.
$model->server->audit($action, function (AuditLog $audit, Server $server) use ($backup) {
$audit->is_system = true;
$audit->metadata = ['backup_uuid' => $backup];
$server->update(['status' => null]);
});

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

Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
->subject($model, $model->server)
->property('name', $model->name)
->log();

return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Backup;
use Pterodactyl\Models\AuditLog;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Facades\Activity;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Pterodactyl\Http\Controllers\Controller;
Expand Down Expand Up @@ -107,7 +109,6 @@ public function resetState(Request $request)
//
// For each of those servers we'll track a new audit log entry to mark them as
// failed and then update them all to be in a valid state.
/** @var \Pterodactyl\Models\Server[] $servers */
$servers = Server::query()
->select('servers.*')
->selectRaw('JSON_UNQUOTE(JSON_EXTRACT(started.metadata, "$.backup_uuid")) as backup_uuid')
Expand All @@ -130,14 +131,17 @@ public function resetState(Request $request)
->where('servers.status', Server::STATUS_RESTORING_BACKUP)
->get();

$backups = Backup::query()->whereIn('uuid', $servers->pluck('backup_uuid'))->get();

/** @var \Pterodactyl\Models\Server $server */
foreach ($servers as $server) {
// Just create a new audit entry for this event and update the server state
// so that power actions, file management, and backups can resume as normal.
$server->audit(AuditLog::SERVER__BACKUP_RESTORE_FAILED, function (AuditLog $audit, Server $server) {
$audit->is_system = true;
$audit->metadata = ['backup_uuid' => $server->getAttribute('backup_uuid')];
$server->update(['status' => null]);
});
$server->update(['status' => null]);

if ($backup = $backups->where('uuid', $server->getAttribute('backup_uuid'))->first()) {
// Just create a new audit entry for this event and update the server state
// so that power actions, file management, and backups can resume as normal.
Activity::event('server:backup.restore-failed')->subject($server, $backup)->log();
}
}

// Update any server marked as installing or restoring as being in a normal state
Expand Down
24 changes: 3 additions & 21 deletions app/Models/ActivityLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@
* @property string|null $description
* @property string|null $actor_type
* @property int|null $actor_id
* @property string|null $subject_type
* @property int|null $subject_id
* @property \Illuminate\Support\Collection $properties
* @property string $timestamp
* @property IlluminateModel|\Eloquent $actor
* @property IlluminateModel|\Eloquent $subject
*
* @method static Builder|ActivityLog forAction(string $action)
* @method static Builder|ActivityLog forEvent(string $event)
* @method static Builder|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
* @method static Builder|ActivityLog forSubject(\Illuminate\Database\Eloquent\Model $subject)
* @method static Builder|ActivityLog newModelQuery()
* @method static Builder|ActivityLog newQuery()
* @method static Builder|ActivityLog query()
Expand All @@ -37,8 +34,6 @@
* @method static Builder|ActivityLog whereId($value)
* @method static Builder|ActivityLog whereIp($value)
* @method static Builder|ActivityLog whereProperties($value)
* @method static Builder|ActivityLog whereSubjectId($value)
* @method static Builder|ActivityLog whereSubjectType($value)
* @method static Builder|ActivityLog whereTimestamp($value)
* @mixin \Eloquent
*/
Expand Down Expand Up @@ -68,14 +63,9 @@ public function actor(): MorphTo
return $this->morphTo();
}

public function subject(): MorphTo
public function scopeForEvent(Builder $builder, string $action): Builder
{
return $this->morphTo();
}

public function scopeForAction(Builder $builder, string $action): Builder
{
return $builder->where('action', $action);
return $builder->where('event', $action);
}

/**
Expand All @@ -85,12 +75,4 @@ public function scopeForActor(Builder $builder, IlluminateModel $actor): Builder
{
return $builder->whereMorphedTo('actor', $actor);
}

/**
* Scopes a query to only return results where the subject is the given model.
*/
public function scopeForSubject(Builder $builder, IlluminateModel $subject): Builder
{
return $builder->whereMorphedTo('subject', $subject);
}
}
40 changes: 40 additions & 0 deletions app/Models/ActivityLogSubject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Pterodactyl\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

/**
* \Pterodactyl\Models\ActivityLogSubject.
*
* @property int $id
* @property int $activity_log_id
* @property int $subject_id
* @property string $subject_type
* @property \Pterodactyl\Models\ActivityLog|null $activityLog
* @property \Illuminate\Database\Eloquent\Model|\Eloquent $subject
*
* @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject query()
* @mixin \Eloquent
*/
class ActivityLogSubject extends Pivot
{
public $incrementing = true;
public $timestamps = false;

protected $table = 'activity_log_subjects';

protected $guarded = ['id'];

public function activityLog()
{
return $this->belongsTo(ActivityLog::class);
}

public function subject()
{
return $this->morphTo();
}
}
47 changes: 4 additions & 43 deletions app/Models/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

namespace Pterodactyl\Models;

use Closure;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause;
use Znck\Eloquent\Traits\BelongsToThrough;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;

/**
Expand Down Expand Up @@ -41,8 +41,6 @@
* @property \Pterodactyl\Models\Allocation|null $allocation
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Allocation[] $allocations
* @property int|null $allocations_count
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\AuditLog[] $audits
* @property int|null $audits_count
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Backup[] $backups
* @property int|null $backups_count
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Database[] $databases
Expand Down Expand Up @@ -373,48 +371,11 @@ public function mounts()
}

/**
* Returns a fresh AuditLog model for the server. This model is not saved to the
* database when created, so it is up to the caller to correctly store it as needed.
*
* @return \Pterodactyl\Models\AuditLog
*/
public function newAuditEvent(string $action, array $metadata = []): AuditLog
{
return AuditLog::instance($action, $metadata)->fill([
'server_id' => $this->id,
]);
}

/**
* Stores a new audit event for a server by using a transaction. If the transaction
* fails for any reason everything executed within will be rolled back. The callback
* passed in will receive the AuditLog model before it is saved and the second argument
* will be the current server instance. The callback should modify the audit entry as
* needed before finishing, any changes will be persisted.
*
* The response from the callback is returned to the caller.
*
* @return mixed
*
* @throws \Throwable
*/
public function audit(string $action, Closure $callback)
{
return $this->getConnection()->transaction(function () use ($action, $callback) {
$model = $this->newAuditEvent($action);
$response = $callback($model, $this);
$model->save();

return $response;
});
}

/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
* Returns all of the activity log entries where the server is the subject.
*/
public function audits()
public function activity(): MorphToMany
{
return $this->hasMany(AuditLog::class);
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
}

/**
Expand Down
Loading

0 comments on commit 2fc5a73

Please sign in to comment.