Add new activity logging code to replace audit log
This commit is contained in:
parent
c14c7b436e
commit
5bb66a00d8
11 changed files with 534 additions and 0 deletions
25
app/Facades/Activity.php
Normal file
25
app/Facades/Activity.php
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Pterodactyl\Services\Activity\ActivityLogService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static ActivityLogService anonymous()
|
||||||
|
* @method static ActivityLogService event(string $action)
|
||||||
|
* @method static ActivityLogService withDescription(?string $description)
|
||||||
|
* @method static ActivityLogService withSubject(Model $subject)
|
||||||
|
* @method static ActivityLogService withActor(Model $actor)
|
||||||
|
* @method static ActivityLogService withProperties(\Illuminate\Support\Collection|array $properties)
|
||||||
|
* @method static ActivityLogService withProperty(string $key, mixed $value)
|
||||||
|
* @method static mixed transaction(\Closure $callback)
|
||||||
|
*/
|
||||||
|
class Activity extends Facade
|
||||||
|
{
|
||||||
|
protected static function getFacadeAccessor()
|
||||||
|
{
|
||||||
|
return ActivityLogService::class;
|
||||||
|
}
|
||||||
|
}
|
20
app/Facades/LogBatch.php
Normal file
20
app/Facades/LogBatch.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use Pterodactyl\Services\Activity\AcitvityLogBatchService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static ?string uuid()
|
||||||
|
* @method static void start()
|
||||||
|
* @method static void end()
|
||||||
|
* @method static mixed transaction(\Closure $callback)
|
||||||
|
*/
|
||||||
|
class LogBatch extends Facade
|
||||||
|
{
|
||||||
|
protected static function getFacadeAccessor()
|
||||||
|
{
|
||||||
|
return AcitvityLogBatchService::class;
|
||||||
|
}
|
||||||
|
}
|
21
app/Facades/LogTarget.php
Normal file
21
app/Facades/LogTarget.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use Pterodactyl\Services\Activity\ActivityLogTargetableService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static void setActor(\Illuminate\Database\Eloquent\Model $actor)
|
||||||
|
* @method static void setSubject(\Illuminate\Database\Eloquent\Model $subject)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Model|null actor()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Model|null subject()
|
||||||
|
* @method static void reset()
|
||||||
|
*/
|
||||||
|
class LogTarget extends Facade
|
||||||
|
{
|
||||||
|
protected static function getFacadeAccessor()
|
||||||
|
{
|
||||||
|
return ActivityLogTargetableService::class;
|
||||||
|
}
|
||||||
|
}
|
30
app/Http/Middleware/ServerActivityLogs.php
Normal file
30
app/Http/Middleware/ServerActivityLogs.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Pterodactyl\Models\Server;
|
||||||
|
use Pterodactyl\Facades\LogTarget;
|
||||||
|
|
||||||
|
class ServerActivityLogs
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Attempts to automatically scope all of the activity log events registered
|
||||||
|
* within the request instance to the given user and server. This only sets
|
||||||
|
* the actor and subject if there is a server present on the request.
|
||||||
|
*
|
||||||
|
* If no server is found this is a no-op as the activity log service can always
|
||||||
|
* set the user based on the authmanager response.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next)
|
||||||
|
{
|
||||||
|
$server = $request->route()->parameter('server');
|
||||||
|
if ($server instanceof Server) {
|
||||||
|
LogTarget::setActor($request->user());
|
||||||
|
LogTarget::setSubject($server);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
94
app/Models/ActivityLog.php
Normal file
94
app/Models/ActivityLog.php
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \Pterodactyl\Models\ActivityLog.
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property string|null $batch
|
||||||
|
* @property string $event
|
||||||
|
* @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 \Illuminate\Database\Eloquent\Model|\Eloquent $actor
|
||||||
|
* @property \Illuminate\Database\Eloquent\Model|\Eloquent $subject
|
||||||
|
*
|
||||||
|
* @method static Builder|ActivityLog forAction(string $action)
|
||||||
|
* @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()
|
||||||
|
* @method static Builder|ActivityLog whereAction($value)
|
||||||
|
* @method static Builder|ActivityLog whereActorId($value)
|
||||||
|
* @method static Builder|ActivityLog whereActorType($value)
|
||||||
|
* @method static Builder|ActivityLog whereBatch($value)
|
||||||
|
* @method static Builder|ActivityLog whereDescription($value)
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
class ActivityLog extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $guarded = [
|
||||||
|
'id',
|
||||||
|
'timestamp',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'properties' => 'collection',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static $validationRules = [
|
||||||
|
'event' => ['required', 'string'],
|
||||||
|
'batch' => ['nullable', 'uuid'],
|
||||||
|
'description' => ['nullable', 'string'],
|
||||||
|
'properties' => ['nullable', 'array'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function actor(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subject(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeForAction(Builder $builder, string $action): Builder
|
||||||
|
{
|
||||||
|
return $builder->where('action', $action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scopes a query to only return results where the actor is a given model.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
20
app/Providers/ActivityLogServiceProvider.php
Normal file
20
app/Providers/ActivityLogServiceProvider.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Pterodactyl\Services\Activity\AcitvityLogBatchService;
|
||||||
|
use Pterodactyl\Services\Activity\ActivityLogTargetableService;
|
||||||
|
|
||||||
|
class ActivityLogServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Registers the necessary activity logger singletons scoped to the individual
|
||||||
|
* request instances.
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
$this->app->scoped(AcitvityLogBatchService::class);
|
||||||
|
$this->app->scoped(ActivityLogTargetableService::class);
|
||||||
|
}
|
||||||
|
}
|
62
app/Services/Activity/AcitvityLogBatchService.php
Normal file
62
app/Services/Activity/AcitvityLogBatchService.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Services\Activity;
|
||||||
|
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
|
class AcitvityLogBatchService
|
||||||
|
{
|
||||||
|
protected int $transaction = 0;
|
||||||
|
protected ?string $uuid = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UUID of the batch, or null if there is not a batch currently
|
||||||
|
* being executed.
|
||||||
|
*/
|
||||||
|
public function uuid(): ?string
|
||||||
|
{
|
||||||
|
return $this->uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new batch transaction. If there is already a transaction present
|
||||||
|
* this will be nested.
|
||||||
|
*/
|
||||||
|
public function start(): void
|
||||||
|
{
|
||||||
|
if ($this->transaction === 0) {
|
||||||
|
$this->uuid = Uuid::uuid4()->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
++$this->transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends a batch transaction, if this is the last transaction in the stack
|
||||||
|
* the UUID will be cleared out.
|
||||||
|
*/
|
||||||
|
public function end(): void
|
||||||
|
{
|
||||||
|
$this->transaction = max(0, $this->transaction - 1);
|
||||||
|
|
||||||
|
if ($this->transaction === 0) {
|
||||||
|
$this->uuid = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the logic provided within the callback in the scope of an activity
|
||||||
|
* log batch transaction.
|
||||||
|
*
|
||||||
|
* @param \Closure $callback
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function transaction(\Closure $callback)
|
||||||
|
{
|
||||||
|
$this->start();
|
||||||
|
$result = $callback($this->uuid());
|
||||||
|
$this->end();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
177
app/Services/Activity/ActivityLogService.php
Normal file
177
app/Services/Activity/ActivityLogService.php
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Services\Activity;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Pterodactyl\Models\ActivityLog;
|
||||||
|
use Illuminate\Contracts\Auth\Factory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\ConnectionInterface;
|
||||||
|
|
||||||
|
class ActivityLogService
|
||||||
|
{
|
||||||
|
protected ?ActivityLog $activity = null;
|
||||||
|
|
||||||
|
protected Factory $manager;
|
||||||
|
protected ConnectionInterface $connection;
|
||||||
|
protected AcitvityLogBatchService $batch;
|
||||||
|
protected ActivityLogTargetableService $targetable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Factory $manager,
|
||||||
|
AcitvityLogBatchService $batch,
|
||||||
|
ActivityLogTargetableService $targetable,
|
||||||
|
ConnectionInterface $connection
|
||||||
|
) {
|
||||||
|
$this->manager = $manager;
|
||||||
|
$this->batch = $batch;
|
||||||
|
$this->targetable = $targetable;
|
||||||
|
$this->connection = $connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the activity logger as having been caused by an anonymous
|
||||||
|
* user type.
|
||||||
|
*/
|
||||||
|
public function anonymous(): self
|
||||||
|
{
|
||||||
|
$this->getActivity()->actor_id = null;
|
||||||
|
$this->getActivity()->actor_type = null;
|
||||||
|
$this->getActivity()->setRelation('actor', null);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the action for this activity log.
|
||||||
|
*/
|
||||||
|
public function event(string $action): self
|
||||||
|
{
|
||||||
|
$this->getActivity()->event = $action;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the description for this activity.
|
||||||
|
*/
|
||||||
|
public function withDescription(?string $description): self
|
||||||
|
{
|
||||||
|
$this->getActivity()->description = $description;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the subject model instance.
|
||||||
|
*/
|
||||||
|
public function withSubject(Model $subject): self
|
||||||
|
{
|
||||||
|
$this->getActivity()->subject()->associate($subject);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the actor model instance.
|
||||||
|
*/
|
||||||
|
public function withActor(Model $actor): self
|
||||||
|
{
|
||||||
|
$this->getActivity()->actor()->associate($actor);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the custom properties for the activity log instance.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Support\Collection|array $properties
|
||||||
|
*/
|
||||||
|
public function withProperties($properties): self
|
||||||
|
{
|
||||||
|
$this->getActivity()->properties = Collection::make($properties);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom property on the activty log instance.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function withProperty(string $key, $value): self
|
||||||
|
{
|
||||||
|
$this->getActivity()->properties = $this->getActivity()->properties->put($key, $value);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs an activity log entry with the set values and then returns the
|
||||||
|
* model instance to the caller.
|
||||||
|
*/
|
||||||
|
public function log(string $description): ActivityLog
|
||||||
|
{
|
||||||
|
$this->withDescription($description);
|
||||||
|
|
||||||
|
$activity = $this->activity;
|
||||||
|
$activity->save();
|
||||||
|
$this->activity = null;
|
||||||
|
|
||||||
|
return $activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the provided callback within the scope of a database transaction
|
||||||
|
* and will only save the activity log entry if everything else succesfully
|
||||||
|
* settles.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function transaction(\Closure $callback, string $description = null)
|
||||||
|
{
|
||||||
|
if (!is_null($description)) {
|
||||||
|
$this->withDescription($description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->connection->transaction(function () use ($callback) {
|
||||||
|
$response = $callback($activity = $this->getActivity());
|
||||||
|
|
||||||
|
$activity->save();
|
||||||
|
$this->activity = null;
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current activity log instance.
|
||||||
|
*/
|
||||||
|
protected function getActivity(): ActivityLog
|
||||||
|
{
|
||||||
|
if ($this->activity) {
|
||||||
|
return $this->activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->activity = new ActivityLog([
|
||||||
|
'batch_uuid' => $this->batch->uuid(),
|
||||||
|
'properties' => Collection::make([]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($subject = $this->targetable->subject()) {
|
||||||
|
$this->withSubject($subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($actor = $this->targetable->actor()) {
|
||||||
|
$this->withActor($actor);
|
||||||
|
} elseif ($user = $this->manager->guard()->user()) {
|
||||||
|
if ($user instanceof Model) {
|
||||||
|
$this->withActor($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->activity;
|
||||||
|
}
|
||||||
|
}
|
47
app/Services/Activity/ActivityLogTargetableService.php
Normal file
47
app/Services/Activity/ActivityLogTargetableService.php
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Services\Activity;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ActivityLogTargetableService
|
||||||
|
{
|
||||||
|
protected ?Model $actor = null;
|
||||||
|
|
||||||
|
protected ?Model $subject = null;
|
||||||
|
|
||||||
|
public function setActor(Model $actor): void
|
||||||
|
{
|
||||||
|
if (!is_null($this->actor)) {
|
||||||
|
throw new InvalidArgumentException('Cannot call ' . __METHOD__ . ' when an actor is already set on the instance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->actor = $actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSubject(Model $subject): void
|
||||||
|
{
|
||||||
|
if (!is_null($this->subject)) {
|
||||||
|
throw new InvalidArgumentException('Cannot call ' . __METHOD__ . ' when a target is already set on the instance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->subject = $subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actor(): ?Model
|
||||||
|
{
|
||||||
|
return $this->actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subject(): ?Model
|
||||||
|
{
|
||||||
|
return $this->subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(): void
|
||||||
|
{
|
||||||
|
$this->actor = null;
|
||||||
|
$this->subject = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -173,6 +173,7 @@ return [
|
||||||
/*
|
/*
|
||||||
* Application Service Providers...
|
* Application Service Providers...
|
||||||
*/
|
*/
|
||||||
|
Pterodactyl\Providers\ActivityLogServiceProvider::class,
|
||||||
Pterodactyl\Providers\AppServiceProvider::class,
|
Pterodactyl\Providers\AppServiceProvider::class,
|
||||||
Pterodactyl\Providers\AuthServiceProvider::class,
|
Pterodactyl\Providers\AuthServiceProvider::class,
|
||||||
Pterodactyl\Providers\BackupsServiceProvider::class,
|
Pterodactyl\Providers\BackupsServiceProvider::class,
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateActivityLogsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('activity_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->uuid('batch')->nullable();
|
||||||
|
$table->string('event')->index();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->nullableNumericMorphs('actor');
|
||||||
|
$table->nullableNumericMorphs('subject');
|
||||||
|
$table->json('properties')->nullable();
|
||||||
|
$table->timestamp('timestamp')->useCurrent()->onUpdate(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('activity_logs');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue