diff --git a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php index ceaa83ebe..c71c4a592 100644 --- a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php +++ b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Illuminate\Http\JsonResponse; +use Pterodactyl\Facades\Activity; use Pterodactyl\Models\Permission; use phpseclib3\Crypt\PublicKeyLoader; use Pterodactyl\Http\Controllers\Controller; @@ -51,6 +52,8 @@ class SftpAuthenticationController extends Controller if ($request->input('type') !== 'public_key') { if (!password_verify($request->input('password'), $user->password)) { + Activity::event('auth:sftp.fail')->property('method', 'password')->subject($user)->log(); + $this->reject($request); } } else { @@ -62,13 +65,29 @@ class SftpAuthenticationController extends Controller } if (!$key || !$user->sshKeys()->where('fingerprint', $key->getFingerprint('sha256'))->exists()) { + // We don't log here because of the way the SFTP system works. This endpoint + // will get hit for every key the user provides, which could be 4 or 5. That is + // a lot of unnecessary log noise. + // + // For now, we'll only log failures due to a bad password as those are not likely + // to occur more than once in a session for the user, and are more likely to be of + // value to the end user. $this->reject($request, is_null($key)); } } $this->validateSftpAccess($user, $server); + Activity::event('auth:sftp.success')->actor($user) + ->subject($user) + ->property(array_filter([ + 'method' => isset($key) ? 'ssh_key' : 'password', + 'fingerprint' => isset($key) ? 'SHA256:' . $key->getFingerprint('sha256') : null, + ])) + ->log(); + return new JsonResponse([ + 'user' => $user->uuid, 'server' => $server->uuid, 'permissions' => $this->permissions->handle($server, $user), ]); @@ -136,6 +155,8 @@ class SftpAuthenticationController extends Controller $permissions = $this->permissions->handle($server, $user); if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) { + Activity::event('server:sftp.denied')->actor($user)->subject($server)->log(); + throw new HttpForbiddenException('You do not have permission to access SFTP for this server.'); } } diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index 090527a37..25a7562f8 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -92,7 +92,7 @@ class ActivityLogTransformer extends BaseClientTransformer $str = trans('activity.' . str_replace(':', '.', $model->event)); preg_match_all('/:(?[\w.-]+\w)(?:[^\w:]?|$)/', $str, $matches); - $exclude = array_merge($matches['key'], ['ip', 'useragent']); + $exclude = array_merge($matches['key'], ['ip', 'useragent', 'using_sftp']); foreach ($model->properties->keys() as $key) { if (!in_array($key, $exclude, true)) { return true; diff --git a/resources/lang/en/activity.php b/resources/lang/en/activity.php index 63d9929ee..51bf77339 100644 --- a/resources/lang/en/activity.php +++ b/resources/lang/en/activity.php @@ -16,6 +16,10 @@ return [ 'recovery-token' => 'Used two-factor recovery token', 'token' => 'Solved two-factor challenge', 'ip-blocked' => 'Blocked request from unlisted IP address for :identifier', + 'sftp' => [ + 'success' => 'Logged in using SFTP', + 'fail' => 'Failed SFTP log in', + ], ], 'user' => [ 'account' => [ @@ -96,6 +100,9 @@ return [ 'update' => 'Updated the ":action" task for the :name schedule', 'delete' => 'Deleted a task for the :name schedule', ], + 'sftp' => [ + 'denied' => 'Blocked SFTP access due to permissions', + ], 'settings' => [ 'rename' => 'Renamed the server from :old to :new', ], diff --git a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx index d6f751572..b12923532 100644 --- a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx +++ b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx @@ -5,7 +5,7 @@ import Translate from '@/components/elements/Translate'; import { format, formatDistanceToNowStrict } from 'date-fns'; import { ActivityLog } from '@definitions/user'; import ActivityLogMetaButton from '@/components/elements/activity/ActivityLogMetaButton'; -import { TerminalIcon } from '@heroicons/react/solid'; +import { FolderOpenIcon, TerminalIcon } from '@heroicons/react/solid'; import classNames from 'classnames'; import style from './style.module.css'; import Avatar from '@/components/Avatar'; @@ -65,10 +65,13 @@ export default ({ activity, children }: Props) => {
{activity.isApi && ( - - - - + + + + )} + {activity.properties.using_sftp && ( + + )} {children} diff --git a/resources/scripts/components/elements/activity/style.module.css b/resources/scripts/components/elements/activity/style.module.css index b779bce0d..066f8cb39 100644 --- a/resources/scripts/components/elements/activity/style.module.css +++ b/resources/scripts/components/elements/activity/style.module.css @@ -1,12 +1,8 @@ .icons { @apply flex space-x-1 mx-2 transition-colors duration-100 text-gray-400; - & > span { - @apply px-1 py-px cursor-pointer hover:text-gray-50; - } - & svg { - @apply w-4 h-4; + @apply px-1 py-px cursor-pointer hover:text-gray-50 h-5 w-auto; } }