Add activity logging for authentication events
This commit is contained in:
		
							parent
							
								
									5bb66a00d8
								
							
						
					
					
						commit
						0999ad7ff0
					
				
							
								
								
									
										18
									
								
								app/Events/Auth/ProvidedAuthenticationToken.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/Events/Auth/ProvidedAuthenticationToken.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace Pterodactyl\Events\Auth; | ||||||
|  | 
 | ||||||
|  | use Pterodactyl\Models\User; | ||||||
|  | 
 | ||||||
|  | class ProvidedAuthenticationToken | ||||||
|  | { | ||||||
|  |     public User $user; | ||||||
|  | 
 | ||||||
|  |     public bool $recovery; | ||||||
|  | 
 | ||||||
|  |     public function __construct(User $user, bool $recovery = false) | ||||||
|  |     { | ||||||
|  |         $this->user = $user; | ||||||
|  |         $this->recovery = $recovery; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,10 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace Pterodactyl\Extensions\Illuminate\Events\Contracts; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Contracts\Events\Dispatcher; | ||||||
|  | 
 | ||||||
|  | interface SubscribesToEvents | ||||||
|  | { | ||||||
|  |     public function subscribe(Dispatcher $events): void; | ||||||
|  | } | ||||||
| @ -9,11 +9,14 @@ use Pterodactyl\Services\Activity\ActivityLogService; | |||||||
| /** | /** | ||||||
|  * @method static ActivityLogService anonymous() |  * @method static ActivityLogService anonymous() | ||||||
|  * @method static ActivityLogService event(string $action) |  * @method static ActivityLogService event(string $action) | ||||||
|  * @method static ActivityLogService withDescription(?string $description) |  * @method static ActivityLogService description(?string $description) | ||||||
|  * @method static ActivityLogService withSubject(Model $subject) |  * @method static ActivityLogService subject(Model $subject) | ||||||
|  * @method static ActivityLogService withActor(Model $actor) |  * @method static ActivityLogService actor(Model $actor) | ||||||
|  * @method static ActivityLogService withProperties(\Illuminate\Support\Collection|array $properties) |  * @method static ActivityLogService withProperties(\Illuminate\Support\Collection|array $properties) | ||||||
|  * @method static ActivityLogService withProperty(string $key, mixed $value) |  * @method static ActivityLogService withRequestMetadata() | ||||||
|  |  * @method static ActivityLogService property(string $key, mixed $value) | ||||||
|  |  * @method static \Pterodactyl\Models\ActivityLog log(string $description = null) | ||||||
|  |  * @method static ActivityLogService clone() | ||||||
|  * @method static mixed transaction(\Closure $callback) |  * @method static mixed transaction(\Closure $callback) | ||||||
|  */ |  */ | ||||||
| class Activity extends Facade | class Activity extends Facade | ||||||
|  | |||||||
| @ -7,8 +7,10 @@ use Carbon\CarbonInterface; | |||||||
| use Pterodactyl\Models\User; | use Pterodactyl\Models\User; | ||||||
| use Illuminate\Http\JsonResponse; | use Illuminate\Http\JsonResponse; | ||||||
| use PragmaRX\Google2FA\Google2FA; | use PragmaRX\Google2FA\Google2FA; | ||||||
|  | use Illuminate\Support\Facades\Event; | ||||||
| use Illuminate\Contracts\Encryption\Encrypter; | use Illuminate\Contracts\Encryption\Encrypter; | ||||||
| use Illuminate\Database\Eloquent\ModelNotFoundException; | use Illuminate\Database\Eloquent\ModelNotFoundException; | ||||||
|  | use Pterodactyl\Events\Auth\ProvidedAuthenticationToken; | ||||||
| use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest; | use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest; | ||||||
| use Illuminate\Contracts\Validation\Factory as ValidationFactory; | use Illuminate\Contracts\Validation\Factory as ValidationFactory; | ||||||
| 
 | 
 | ||||||
| @ -72,12 +74,16 @@ class LoginCheckpointController extends AbstractLoginController | |||||||
|         // Recovery tokens go through a slightly different pathway for usage.
 |         // Recovery tokens go through a slightly different pathway for usage.
 | ||||||
|         if (!is_null($recoveryToken = $request->input('recovery_token'))) { |         if (!is_null($recoveryToken = $request->input('recovery_token'))) { | ||||||
|             if ($this->isValidRecoveryToken($user, $recoveryToken)) { |             if ($this->isValidRecoveryToken($user, $recoveryToken)) { | ||||||
|  |                 Event::dispatch(new ProvidedAuthenticationToken($user, true)); | ||||||
|  | 
 | ||||||
|                 return $this->sendLoginResponse($user, $request); |                 return $this->sendLoginResponse($user, $request); | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             $decrypted = $this->encrypter->decrypt($user->totp_secret); |             $decrypted = $this->encrypter->decrypt($user->totp_secret); | ||||||
| 
 | 
 | ||||||
|             if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { |             if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { | ||||||
|  |                 Event::dispatch(new ProvidedAuthenticationToken($user)); | ||||||
|  | 
 | ||||||
|                 return $this->sendLoginResponse($user, $request); |                 return $this->sendLoginResponse($user, $request); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ use Illuminate\Support\Str; | |||||||
| use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||||
| use Pterodactyl\Models\User; | use Pterodactyl\Models\User; | ||||||
| use Illuminate\Http\JsonResponse; | use Illuminate\Http\JsonResponse; | ||||||
|  | use Pterodactyl\Facades\Activity; | ||||||
| use Illuminate\Contracts\View\View; | use Illuminate\Contracts\View\View; | ||||||
| use Illuminate\Contracts\View\Factory as ViewFactory; | use Illuminate\Contracts\View\Factory as ViewFactory; | ||||||
| use Illuminate\Database\Eloquent\ModelNotFoundException; | use Illuminate\Database\Eloquent\ModelNotFoundException; | ||||||
| @ -71,6 +72,8 @@ class LoginController extends AbstractLoginController | |||||||
|             return $this->sendLoginResponse($user, $request); |             return $this->sendLoginResponse($user, $request); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         Activity::event('login.checkpoint')->withRequestMetadata()->subject($user)->log(); | ||||||
|  | 
 | ||||||
|         $request->session()->put('auth_confirmation_token', [ |         $request->session()->put('auth_confirmation_token', [ | ||||||
|             'user_id' => $user->id, |             'user_id' => $user->id, | ||||||
|             'token_value' => $token = Str::random(64), |             'token_value' => $token = Str::random(64), | ||||||
|  | |||||||
							
								
								
									
										40
									
								
								app/Listeners/Auth/AuthenticationListener.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/Listeners/Auth/AuthenticationListener.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace Pterodactyl\Listeners\Auth; | ||||||
|  | 
 | ||||||
|  | use Pterodactyl\Facades\Activity; | ||||||
|  | use Illuminate\Auth\Events\Login; | ||||||
|  | use Illuminate\Auth\Events\Failed; | ||||||
|  | use Illuminate\Contracts\Events\Dispatcher; | ||||||
|  | use Pterodactyl\Extensions\Illuminate\Events\Contracts\SubscribesToEvents; | ||||||
|  | 
 | ||||||
|  | class AuthenticationListener implements SubscribesToEvents | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Handles an authentication event by logging the user and information about | ||||||
|  |      * the request. | ||||||
|  |      * | ||||||
|  |      * @param \Illuminate\Auth\Events\Login|\Illuminate\Auth\Events\Failed $event | ||||||
|  |      */ | ||||||
|  |     public function handle($event): void | ||||||
|  |     { | ||||||
|  |         $activity = Activity::withRequestMetadata(); | ||||||
|  |         if ($event->user) { | ||||||
|  |             $activity = $activity->subject($event->user); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($event instanceof Failed) { | ||||||
|  |             foreach ($event->credentials as $key => $value) { | ||||||
|  |                 $activity = $activity->property($key, $value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $activity->event($event instanceof Failed ? 'login.failed' : 'login.success')->log(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function subscribe(Dispatcher $events): void | ||||||
|  |     { | ||||||
|  |         $events->listen(Failed::class, self::class); | ||||||
|  |         $events->listen(Login::class, self::class); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								app/Listeners/Auth/PasswordResetListener.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/Listeners/Auth/PasswordResetListener.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace Pterodactyl\Listeners\Auth; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Pterodactyl\Facades\Activity; | ||||||
|  | use Illuminate\Auth\Events\PasswordReset; | ||||||
|  | 
 | ||||||
|  | class PasswordResetListener | ||||||
|  | { | ||||||
|  |     protected Request $request; | ||||||
|  | 
 | ||||||
|  |     public function __construct(Request $request) | ||||||
|  |     { | ||||||
|  |         $this->request = $request; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handle(PasswordReset $event) | ||||||
|  |     { | ||||||
|  |         Activity::event('login.password-reset') | ||||||
|  |             ->withRequestMetadata() | ||||||
|  |             ->subject($event->user) | ||||||
|  |             ->log(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								app/Listeners/Auth/TwoFactorListener.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/Listeners/Auth/TwoFactorListener.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace Pterodactyl\Listeners\Auth; | ||||||
|  | 
 | ||||||
|  | use Pterodactyl\Facades\Activity; | ||||||
|  | use Pterodactyl\Events\Auth\ProvidedAuthenticationToken; | ||||||
|  | 
 | ||||||
|  | class TwoFactorListener | ||||||
|  | { | ||||||
|  |     public function handle(ProvidedAuthenticationToken $event) | ||||||
|  |     { | ||||||
|  |         Activity::event($event->recovery ? 'login.recovery-token' : 'login.token') | ||||||
|  |             ->withRequestMetadata() | ||||||
|  |             ->subject($event->user) | ||||||
|  |             ->log(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -3,6 +3,7 @@ | |||||||
| namespace Pterodactyl\Models; | namespace Pterodactyl\Models; | ||||||
| 
 | 
 | ||||||
| use Pterodactyl\Rules\Username; | use Pterodactyl\Rules\Username; | ||||||
|  | use Pterodactyl\Facades\Activity; | ||||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||||
| use Illuminate\Validation\Rules\In; | use Illuminate\Validation\Rules\In; | ||||||
| use Illuminate\Auth\Authenticatable; | use Illuminate\Auth\Authenticatable; | ||||||
| @ -214,6 +215,11 @@ class User extends Model implements | |||||||
|      */ |      */ | ||||||
|     public function sendPasswordResetNotification($token) |     public function sendPasswordResetNotification($token) | ||||||
|     { |     { | ||||||
|  |         Activity::event('login.reset-password') | ||||||
|  |             ->withRequestMetadata() | ||||||
|  |             ->subject($this) | ||||||
|  |             ->log('sending password reset email'); | ||||||
|  | 
 | ||||||
|         $this->notify(new ResetPasswordNotification($token)); |         $this->notify(new ResetPasswordNotification($token)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ use Pterodactyl\Observers\UserObserver; | |||||||
| use Pterodactyl\Observers\ServerObserver; | use Pterodactyl\Observers\ServerObserver; | ||||||
| use Pterodactyl\Observers\SubuserObserver; | use Pterodactyl\Observers\SubuserObserver; | ||||||
| use Pterodactyl\Observers\EggVariableObserver; | use Pterodactyl\Observers\EggVariableObserver; | ||||||
|  | use Pterodactyl\Listeners\Auth\AuthenticationListener; | ||||||
| use Pterodactyl\Events\Server\Installed as ServerInstalledEvent; | use Pterodactyl\Events\Server\Installed as ServerInstalledEvent; | ||||||
| use Pterodactyl\Notifications\ServerInstalled as ServerInstalledNotification; | use Pterodactyl\Notifications\ServerInstalled as ServerInstalledNotification; | ||||||
| use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; | use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; | ||||||
| @ -22,9 +23,11 @@ class EventServiceProvider extends ServiceProvider | |||||||
|      * @var array |      * @var array | ||||||
|      */ |      */ | ||||||
|     protected $listen = [ |     protected $listen = [ | ||||||
|         ServerInstalledEvent::class => [ |         ServerInstalledEvent::class => [ServerInstalledNotification::class], | ||||||
|             ServerInstalledNotification::class, |     ]; | ||||||
|         ], | 
 | ||||||
|  |     protected $subscribe = [ | ||||||
|  |         AuthenticationListener::class, | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -39,4 +42,9 @@ class EventServiceProvider extends ServiceProvider | |||||||
|         Subuser::observe(SubuserObserver::class); |         Subuser::observe(SubuserObserver::class); | ||||||
|         EggVariable::observe(EggVariableObserver::class); |         EggVariable::observe(EggVariableObserver::class); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function shouldDiscoverEvents() | ||||||
|  |     { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ use Illuminate\Support\Collection; | |||||||
| use Pterodactyl\Models\ActivityLog; | use Pterodactyl\Models\ActivityLog; | ||||||
| use Illuminate\Contracts\Auth\Factory; | use Illuminate\Contracts\Auth\Factory; | ||||||
| use Illuminate\Database\Eloquent\Model; | use Illuminate\Database\Eloquent\Model; | ||||||
|  | use Illuminate\Support\Facades\Request; | ||||||
| use Illuminate\Database\ConnectionInterface; | use Illuminate\Database\ConnectionInterface; | ||||||
| 
 | 
 | ||||||
| class ActivityLogService | class ActivityLogService | ||||||
| @ -55,7 +56,7 @@ class ActivityLogService | |||||||
|     /** |     /** | ||||||
|      * Set the description for this activity. |      * Set the description for this activity. | ||||||
|      */ |      */ | ||||||
|     public function withDescription(?string $description): self |     public function description(?string $description): self | ||||||
|     { |     { | ||||||
|         $this->getActivity()->description = $description; |         $this->getActivity()->description = $description; | ||||||
| 
 | 
 | ||||||
| @ -65,7 +66,7 @@ class ActivityLogService | |||||||
|     /** |     /** | ||||||
|      * Sets the subject model instance. |      * Sets the subject model instance. | ||||||
|      */ |      */ | ||||||
|     public function withSubject(Model $subject): self |     public function subject(Model $subject): self | ||||||
|     { |     { | ||||||
|         $this->getActivity()->subject()->associate($subject); |         $this->getActivity()->subject()->associate($subject); | ||||||
| 
 | 
 | ||||||
| @ -75,7 +76,7 @@ class ActivityLogService | |||||||
|     /** |     /** | ||||||
|      * Sets the actor model instance. |      * Sets the actor model instance. | ||||||
|      */ |      */ | ||||||
|     public function withActor(Model $actor): self |     public function actor(Model $actor): self | ||||||
|     { |     { | ||||||
|         $this->getActivity()->actor()->associate($actor); |         $this->getActivity()->actor()->associate($actor); | ||||||
| 
 | 
 | ||||||
| @ -99,28 +100,52 @@ class ActivityLogService | |||||||
|      * |      * | ||||||
|      * @param mixed $value |      * @param mixed $value | ||||||
|      */ |      */ | ||||||
|     public function withProperty(string $key, $value): self |     public function property(string $key, $value): self | ||||||
|     { |     { | ||||||
|         $this->getActivity()->properties = $this->getActivity()->properties->put($key, $value); |         $this->getActivity()->properties = $this->getActivity()->properties->put($key, $value); | ||||||
| 
 | 
 | ||||||
|         return $this; |         return $this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Attachs the instance request metadata to the activity log event. | ||||||
|  |      */ | ||||||
|  |     public function withRequestMetadata(): self | ||||||
|  |     { | ||||||
|  |         $this->property('ip', Request::getClientIp()); | ||||||
|  |         $this->property('useragent', Request::userAgent()); | ||||||
|  | 
 | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Logs an activity log entry with the set values and then returns the |      * Logs an activity log entry with the set values and then returns the | ||||||
|      * model instance to the caller. |      * model instance to the caller. | ||||||
|      */ |      */ | ||||||
|     public function log(string $description): ActivityLog |     public function log(string $description = null): ActivityLog | ||||||
|     { |     { | ||||||
|         $this->withDescription($description); |         $activity = $this->getActivity(); | ||||||
|  | 
 | ||||||
|  |         if (!is_null($description)) { | ||||||
|  |             $activity->description = $description; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         $activity = $this->activity; |  | ||||||
|         $activity->save(); |         $activity->save(); | ||||||
|  | 
 | ||||||
|         $this->activity = null; |         $this->activity = null; | ||||||
| 
 | 
 | ||||||
|         return $activity; |         return $activity; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns a cloned instance of the service allowing for the creation of a base | ||||||
|  |      * activity log with the ability to change values on the fly without impact. | ||||||
|  |      */ | ||||||
|  |     public function clone(): self | ||||||
|  |     { | ||||||
|  |         return clone $this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Executes the provided callback within the scope of a database transaction |      * Executes the provided callback within the scope of a database transaction | ||||||
|      * and will only save the activity log entry if everything else succesfully |      * and will only save the activity log entry if everything else succesfully | ||||||
| @ -133,7 +158,7 @@ class ActivityLogService | |||||||
|     public function transaction(\Closure $callback, string $description = null) |     public function transaction(\Closure $callback, string $description = null) | ||||||
|     { |     { | ||||||
|         if (!is_null($description)) { |         if (!is_null($description)) { | ||||||
|             $this->withDescription($description); |             $this->description($description); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return $this->connection->transaction(function () use ($callback) { |         return $this->connection->transaction(function () use ($callback) { | ||||||
| @ -161,14 +186,14 @@ class ActivityLogService | |||||||
|         ]); |         ]); | ||||||
| 
 | 
 | ||||||
|         if ($subject = $this->targetable->subject()) { |         if ($subject = $this->targetable->subject()) { | ||||||
|             $this->withSubject($subject); |             $this->subject($subject); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if ($actor = $this->targetable->actor()) { |         if ($actor = $this->targetable->actor()) { | ||||||
|             $this->withActor($actor); |             $this->actor($actor); | ||||||
|         } elseif ($user = $this->manager->guard()->user()) { |         } elseif ($user = $this->manager->guard()->user()) { | ||||||
|             if ($user instanceof Model) { |             if ($user instanceof Model) { | ||||||
|                 $this->withActor($user); |                 $this->actor($user); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DaneEveritt
						DaneEveritt