From 0520014c0f3f05ba75212a639242456b8ed2492f Mon Sep 17 00:00:00 2001
From: DaneEveritt <dane@daneeveritt.com>
Date: Sat, 18 Jun 2022 12:07:44 -0400
Subject: [PATCH] Add support for tracking when an activity event is triggered
 from an API key

---
 app/Http/Kernel.php                           |  2 ++
 app/Http/Middleware/Activity/TrackAPIKey.php  | 30 ++++++++++++++++++
 app/Models/ActivityLog.php                    |  9 ++++++
 app/Services/Activity/ActivityLogService.php  |  1 +
 .../Activity/ActivityLogTargetableService.php | 13 ++++++++
 .../Api/Client/ActivityLogTransformer.php     |  1 +
 ...rack_api_key_usage_for_activity_events.php | 31 +++++++++++++++++++
 7 files changed, 87 insertions(+)
 create mode 100644 app/Http/Middleware/Activity/TrackAPIKey.php
 create mode 100644 database/migrations/2022_06_18_112822_track_api_key_usage_for_activity_events.php

diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index bbd5d2ce..8084dc22 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -16,6 +16,7 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
 use Pterodactyl\Http\Middleware\LanguageMiddleware;
 use Illuminate\Foundation\Http\Kernel as HttpKernel;
 use Illuminate\Routing\Middleware\SubstituteBindings;
+use Pterodactyl\Http\Middleware\Activity\TrackAPIKey;
 use Illuminate\Session\Middleware\AuthenticateSession;
 use Illuminate\View\Middleware\ShareErrorsFromSession;
 use Pterodactyl\Http\Middleware\MaintenanceMiddleware;
@@ -68,6 +69,7 @@ class Kernel extends HttpKernel
             EnsureStatefulRequests::class,
             'auth:sanctum',
             IsValidJson::class,
+            TrackAPIKey::class,
             RequireTwoFactorAuthentication::class,
             AuthenticateIPAccess::class,
         ],
diff --git a/app/Http/Middleware/Activity/TrackAPIKey.php b/app/Http/Middleware/Activity/TrackAPIKey.php
new file mode 100644
index 00000000..90d53759
--- /dev/null
+++ b/app/Http/Middleware/Activity/TrackAPIKey.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Pterodactyl\Http\Middleware\Activity;
+
+use Closure;
+use Illuminate\Http\Request;
+use Pterodactyl\Models\ApiKey;
+use Pterodactyl\Facades\LogTarget;
+
+class TrackAPIKey
+{
+    /**
+     * Determines if the authenticated user making this request is using an actual
+     * API key, or it is just a cookie authenticated session. This data is set in a
+     * request singleton so that all tracked activity log events are properly associated
+     * with the given API key.
+     *
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        if ($request->user()) {
+            $token = $request->user()->currentAccessToken();
+
+            LogTarget::setApiKeyId($token instanceof ApiKey ? $token->id : null);
+        }
+
+        return $next($request);
+    }
+}
diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php
index dbf936a6..bfe324ad 100644
--- a/app/Models/ActivityLog.php
+++ b/app/Models/ActivityLog.php
@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Event;
 use Pterodactyl\Events\ActivityLogged;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\MassPrunable;
+use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Database\Eloquent\Relations\MorphTo;
 use Illuminate\Database\Eloquent\Model as IlluminateModel;
 
@@ -21,11 +22,13 @@ use Illuminate\Database\Eloquent\Model as IlluminateModel;
  * @property string|null $description
  * @property string|null $actor_type
  * @property int|null $actor_id
+ * @property int|null $api_key_id
  * @property \Illuminate\Support\Collection|null $properties
  * @property \Carbon\Carbon $timestamp
  * @property IlluminateModel|\Eloquent $actor
  * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\ActivityLogSubject[] $subjects
  * @property int|null $subjects_count
+ * @property \Pterodactyl\Models\ApiKey|null $apiKey
  *
  * @method static Builder|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
  * @method static Builder|ActivityLog forEvent(string $action)
@@ -34,6 +37,7 @@ use Illuminate\Database\Eloquent\Model as IlluminateModel;
  * @method static Builder|ActivityLog query()
  * @method static Builder|ActivityLog whereActorId($value)
  * @method static Builder|ActivityLog whereActorType($value)
+ * @method static Builder|ActivityLog whereApiKeyId($value)
  * @method static Builder|ActivityLog whereBatch($value)
  * @method static Builder|ActivityLog whereDescription($value)
  * @method static Builder|ActivityLog whereEvent($value)
@@ -86,6 +90,11 @@ class ActivityLog extends Model
         return $this->hasMany(ActivityLogSubject::class);
     }
 
+    public function apiKey(): HasOne
+    {
+        return $this->hasOne(ApiKey::class, 'id', 'api_key_id');
+    }
+
     public function scopeForEvent(Builder $builder, string $action): Builder
     {
         return $builder->where('event', $action);
diff --git a/app/Services/Activity/ActivityLogService.php b/app/Services/Activity/ActivityLogService.php
index cb45dc33..cfdd5a9c 100644
--- a/app/Services/Activity/ActivityLogService.php
+++ b/app/Services/Activity/ActivityLogService.php
@@ -210,6 +210,7 @@ class ActivityLogService
             'ip' => Request::ip(),
             'batch_uuid' => $this->batch->uuid(),
             'properties' => Collection::make([]),
+            'api_key_id' => $this->targetable->apiKeyId(),
         ]);
 
         if ($subject = $this->targetable->subject()) {
diff --git a/app/Services/Activity/ActivityLogTargetableService.php b/app/Services/Activity/ActivityLogTargetableService.php
index a4da5b5f..4d37e982 100644
--- a/app/Services/Activity/ActivityLogTargetableService.php
+++ b/app/Services/Activity/ActivityLogTargetableService.php
@@ -10,6 +10,8 @@ class ActivityLogTargetableService
 
     protected ?Model $subject = null;
 
+    protected ?int $apiKeyId = null;
+
     public function setActor(Model $actor): void
     {
         $this->actor = $actor;
@@ -20,6 +22,11 @@ class ActivityLogTargetableService
         $this->subject = $subject;
     }
 
+    public function setApiKeyId(?int $apiKeyId): void
+    {
+        $this->apiKeyId = $apiKeyId;
+    }
+
     public function actor(): ?Model
     {
         return $this->actor;
@@ -30,9 +37,15 @@ class ActivityLogTargetableService
         return $this->subject;
     }
 
+    public function apiKeyId(): ?int
+    {
+        return $this->apiKeyId;
+    }
+
     public function reset(): void
     {
         $this->actor = null;
         $this->subject = null;
+        $this->apiKeyId = null;
     }
 }
diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php
index e6ff72a6..1a0154b6 100644
--- a/app/Transformers/Api/Client/ActivityLogTransformer.php
+++ b/app/Transformers/Api/Client/ActivityLogTransformer.php
@@ -19,6 +19,7 @@ class ActivityLogTransformer extends BaseClientTransformer
         return [
             'batch' => $model->batch,
             'event' => $model->event,
+            'is_api' => !is_null($model->api_key_id),
             'ip' => $model->ip,
             'description' => $model->description,
             'properties' => $model->properties ? $model->properties->toArray() : [],
diff --git a/database/migrations/2022_06_18_112822_track_api_key_usage_for_activity_events.php b/database/migrations/2022_06_18_112822_track_api_key_usage_for_activity_events.php
new file mode 100644
index 00000000..6e35df9a
--- /dev/null
+++ b/database/migrations/2022_06_18_112822_track_api_key_usage_for_activity_events.php
@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+return new class () extends Migration {
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('activity_logs', function (Blueprint $table) {
+            $table->unsignedInteger('api_key_id')->nullable()->after('actor_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('activity_logs', function (Blueprint $table) {
+            $table->dropColumn('api_key_id');
+        });
+    }
+};