From 058e490ec42664406bc4ed3b5c00d2878a2c5f11 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 25 Oct 2017 00:35:25 -0400 Subject: [PATCH] Implement Panel changes to support internal SFTP subsystem on Daemon (#703) --- .../Controllers/API/Remote/SftpController.php | 85 ++++++++ .../Server/Settings/SftpController.php | 26 +++ .../Middleware/Daemon/DaemonAuthenticate.php | 2 +- .../Remote/SftpAuthenticationFormRequest.php | 44 +++++ app/Models/Node.php | 12 +- app/Models/Server.php | 17 +- .../Servers/ServerCreationService.php | 10 - .../Servers/UsernameGenerationService.php | 40 ---- .../Sftp/AuthenticateUsingPasswordService.php | 90 +++++++++ .../Controllers/JavascriptInjection.php | 1 - database/factories/ModelFactory.php | 2 - ..._24_222238_RemoveLegacySFTPInformation.php | 32 ++++ resources/lang/en/server.php | 2 +- .../pterodactyl/admin/servers/index.blade.php | 2 - .../admin/servers/view/index.blade.php | 8 - .../pterodactyl/server/console.blade.php | 2 +- .../themes/pterodactyl/server/index.blade.php | 2 +- .../server/settings/sftp.blade.php | 44 +---- routes/api-remote.php | 4 + routes/server.php | 5 +- .../Servers/ServerCreationServiceTest.php | 11 -- .../Servers/UsernameGenerationServiceTest.php | 109 ----------- .../AuthenticateUsingPasswordServiceTest.php | 181 ++++++++++++++++++ 23 files changed, 484 insertions(+), 247 deletions(-) create mode 100644 app/Http/Controllers/API/Remote/SftpController.php create mode 100644 app/Http/Controllers/Server/Settings/SftpController.php create mode 100644 app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php delete mode 100644 app/Services/Servers/UsernameGenerationService.php create mode 100644 app/Services/Sftp/AuthenticateUsingPasswordService.php create mode 100644 database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php delete mode 100644 tests/Unit/Services/Servers/UsernameGenerationServiceTest.php create mode 100644 tests/Unit/Services/Sftp/AuthenticateUsingPasswordServiceTest.php diff --git a/app/Http/Controllers/API/Remote/SftpController.php b/app/Http/Controllers/API/Remote/SftpController.php new file mode 100644 index 000000000..07582153d --- /dev/null +++ b/app/Http/Controllers/API/Remote/SftpController.php @@ -0,0 +1,85 @@ +authenticationService = $authenticationService; + } + + /** + * Authenticate a set of credentials and return the associated server details + * for a SFTP connection on the daemon. + * + * @param \Pterodactyl\Http\Requests\API\Remote\SftpAuthenticationFormRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function index(SftpAuthenticationFormRequest $request): JsonResponse + { + $connection = explode('.', $request->input('username')); + $this->incrementLoginAttempts($request); + + if ($this->hasTooManyLoginAttempts($request)) { + return response()->json([ + 'error' => 'Logins throttled.', + ], 429); + } + + try { + $data = $this->authenticationService->handle( + array_get($connection, 0), + $request->input('password'), + object_get($request->attributes->get('node'), 'id', 0), + array_get($connection, 1) + ); + + $this->clearLoginAttempts($request); + } catch (AuthenticationException $exception) { + return response()->json([ + 'error' => 'Invalid credentials.', + ], 403); + } catch (RecordNotFoundException $exception) { + return response()->json([ + 'error' => 'Invalid server.', + ], 404); + } + + return response()->json($data); + } + + /** + * Get the throttle key for the given request. + * + * @param \Illuminate\Http\Request $request + * @return string + */ + protected function throttleKey(Request $request) + { + return strtolower(array_get(explode('.', $request->input('username')), 0) . '|' . $request->ip()); + } +} diff --git a/app/Http/Controllers/Server/Settings/SftpController.php b/app/Http/Controllers/Server/Settings/SftpController.php new file mode 100644 index 000000000..b128ba5c9 --- /dev/null +++ b/app/Http/Controllers/Server/Settings/SftpController.php @@ -0,0 +1,26 @@ +setRequest($request)->injectJavascript(); + + return view('server.settings.sftp'); + } +} diff --git a/app/Http/Middleware/Daemon/DaemonAuthenticate.php b/app/Http/Middleware/Daemon/DaemonAuthenticate.php index 2804fa923..2572ba854 100644 --- a/app/Http/Middleware/Daemon/DaemonAuthenticate.php +++ b/app/Http/Middleware/Daemon/DaemonAuthenticate.php @@ -75,7 +75,7 @@ class DaemonAuthenticate throw new HttpException(403); } - $request->attributes->set('node.model', $node); + $request->attributes->set('node', $node); return $next($request); } diff --git a/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php b/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php new file mode 100644 index 000000000..5d82f55c7 --- /dev/null +++ b/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php @@ -0,0 +1,44 @@ + 'required|string', + 'password' => 'required|string', + ]; + } + + /** + * Return only the fields that we are interested in from the request. + * This will include empty fields as a null value. + * + * @return array + */ + public function normalize() + { + return $this->only( + array_keys($this->rules()) + ); + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 368c6f3d8..71f5614b5 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -144,13 +144,23 @@ class Node extends Model implements CleansAttributes, ValidableContract ], ], 'docker' => [ + 'container' => [ + 'user' => null, + ], + 'network' => [ + 'name' => 'pterodactyl_nw', + ], 'socket' => '/var/run/docker.sock', 'autoupdate_images' => true, ], 'sftp' => [ 'path' => $this->daemonBase, + 'ip' => '0.0.0.0', 'port' => $this->daemonSFTP, - 'container' => 'ptdl-sftp', + 'keypair' => [ + 'bits' => 2048, + 'e' => 65537, + ], ], 'logger' => [ 'path' => 'logs/', diff --git a/app/Models/Server.php b/app/Models/Server.php index 09563baf1..04ac19e43 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -29,19 +29,12 @@ class Server extends Model implements CleansAttributes, ValidableContract */ protected $table = 'servers'; - /** - * The attributes excluded from the model's JSON form. - * - * @var array - */ - protected $hidden = ['sftp_password']; - /** * The attributes that should be mutated to dates. * * @var array */ - protected $dates = ['deleted_at']; + protected $dates = [self::CREATED_AT, self::UPDATED_AT, 'deleted_at']; /** * Always eager load these relationships on the model. @@ -55,7 +48,7 @@ class Server extends Model implements CleansAttributes, ValidableContract * * @var array */ - protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; + protected $guarded = ['id', 'installed', self::CREATED_AT, self::UPDATED_AT, 'deleted_at']; /** * @var array @@ -73,8 +66,6 @@ class Server extends Model implements CleansAttributes, ValidableContract 'node_id' => 'required', 'allocation_id' => 'required', 'pack_id' => 'sometimes', - 'auto_deploy' => 'sometimes', - 'custom_id' => 'sometimes', 'skip_scripts' => 'sometimes', ]; @@ -95,10 +86,7 @@ class Server extends Model implements CleansAttributes, ValidableContract 'nest_id' => 'exists:nests,id', 'egg_id' => 'exists:eggs,id', 'pack_id' => 'nullable|numeric|min:0', - 'custom_container' => 'nullable|string', 'startup' => 'nullable|string', - 'auto_deploy' => 'accepted', - 'custom_id' => 'numeric|unique:servers,id', 'skip_scripts' => 'boolean', ]; @@ -132,7 +120,6 @@ class Server extends Model implements CleansAttributes, ValidableContract */ protected $searchableColumns = [ 'name' => 10, - 'username' => 10, 'uuidShort' => 9, 'uuid' => 8, 'pack.name' => 7, diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 0994abe55..33d23b000 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -63,11 +63,6 @@ class ServerCreationService */ protected $userRepository; - /** - * @var \Pterodactyl\Services\Servers\UsernameGenerationService - */ - protected $usernameService; - /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ @@ -84,7 +79,6 @@ class ServerCreationService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository - * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService */ public function __construct( @@ -96,7 +90,6 @@ class ServerCreationService ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, - UsernameGenerationService $usernameService, VariableValidatorService $validatorService ) { $this->allocationRepository = $allocationRepository; @@ -107,7 +100,6 @@ class ServerCreationService $this->repository = $repository; $this->serverVariableRepository = $serverVariableRepository; $this->userRepository = $userRepository; - $this->usernameService = $usernameService; $this->validatorService = $validatorService; } @@ -151,8 +143,6 @@ class ServerCreationService 'startup' => $data['startup'], 'daemonSecret' => str_random(NodeCreationService::DAEMON_SECRET_LENGTH), 'image' => $data['docker_image'], - 'username' => $this->usernameService->generate($data['name'], $uniqueShort), - 'sftp_password' => null, ]); // Process allocations and assign them to the server in the database. diff --git a/app/Services/Servers/UsernameGenerationService.php b/app/Services/Servers/UsernameGenerationService.php deleted file mode 100644 index 3fb2c6d3b..000000000 --- a/app/Services/Servers/UsernameGenerationService.php +++ /dev/null @@ -1,40 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Services\Servers; - -class UsernameGenerationService -{ - /** - * Generate a unique username to be used for SFTP connections and identification - * of the server docker container on the host system. - * - * @param string $name - * @param null $identifier - * @return string - */ - public function generate($name, $identifier = null) - { - if (is_null($identifier) || ! ctype_alnum($identifier)) { - $unique = str_random(8); - } else { - if (strlen($identifier) < 8) { - $unique = $identifier . str_random((8 - strlen($identifier))); - } else { - $unique = substr($identifier, 0, 8); - } - } - - // Filter the Server Name - $name = trim(preg_replace('/[^A-Za-z0-9]+/', '', $name), '_'); - $name = (strlen($name) < 1) ? str_random(6) : $name; - - return strtolower(substr($name, 0, 6) . '_' . $unique); - } -} diff --git a/app/Services/Sftp/AuthenticateUsingPasswordService.php b/app/Services/Sftp/AuthenticateUsingPasswordService.php new file mode 100644 index 000000000..487d251d4 --- /dev/null +++ b/app/Services/Sftp/AuthenticateUsingPasswordService.php @@ -0,0 +1,90 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + $this->userRepository = $userRepository; + } + + /** + * Attempt to authenticate a provded username and password and determine if they + * have permission to access a given server. This function does not account for + * subusers currently. Only administrators and server owners can login to access + * their files at this time. + * + * Server must exist on the node that the API call is being made from in order for a + * valid response to be provided. + * + * @param string $username + * @param string $password + * @param string|null $server + * @param int $node + * @return array + * + * @throws \Illuminate\Auth\AuthenticationException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(string $username, string $password, int $node, string $server = null): array + { + if (is_null($server)) { + throw new RecordNotFoundException; + } + + try { + $user = $this->userRepository->withColumns(['id', 'root_admin', 'password'])->findFirstWhere([['username', '=', $username]]); + + if (! password_verify($password, $user->password)) { + throw new AuthenticationException; + } + } catch (RecordNotFoundException $exception) { + throw new AuthenticationException; + } + + $server = $this->repository->withColumns(['id', 'node_id', 'owner_id', 'uuid'])->getByUuid($server); + if ($server->node_id !== $node || (! $user->root_admin && $server->owner_id !== $user->id)) { + throw new RecordNotFoundException; + } + + return [ + 'server' => $server->uuid, + 'token' => $this->keyProviderService->handle($server->id, $user->id), + ]; + } +} diff --git a/app/Traits/Controllers/JavascriptInjection.php b/app/Traits/Controllers/JavascriptInjection.php index 7c7ee3c16..c6efc86ac 100644 --- a/app/Traits/Controllers/JavascriptInjection.php +++ b/app/Traits/Controllers/JavascriptInjection.php @@ -50,7 +50,6 @@ trait JavascriptInjection 'uuid' => $server->uuid, 'uuidShort' => $server->uuidShort, 'daemonSecret' => $token, - 'username' => $server->username, ], 'node' => [ 'fqdn' => $server->node->fqdn, diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 82d7ad8b0..233aeee01 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -30,8 +30,6 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa 'cpu' => 0, 'oom_disabled' => 0, 'pack_id' => null, - 'username' => $faker->userName, - 'sftp_password' => null, 'installed' => 1, 'created_at' => \Carbon\Carbon::now(), 'updated_at' => \Carbon\Carbon::now(), diff --git a/database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php b/database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php new file mode 100644 index 000000000..e41acd275 --- /dev/null +++ b/database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php @@ -0,0 +1,32 @@ +dropUnique(['username']); + + $table->dropColumn('username'); + $table->dropColumn('sftp_password'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->string('username')->nullable()->after('image')->unique(); + $table->text('sftp_password')->after('image'); + }); + } +} diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index 489803b87..3ed89c511 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -301,7 +301,7 @@ return [ 'change_pass' => 'Change SFTP Password', 'details' => 'SFTP Details', 'conn_addr' => 'Connection Address', - 'warning' => 'Ensure that your client is set to use SFTP and not FTP or FTPS for connections, there is a difference between the protocols.', + 'warning' => 'The SFTP password is your account password. Ensure that your client is set to use SFTP and not FTP or FTPS for connections, there is a difference between the protocols.', ], 'database' => [ 'header' => 'Databases', diff --git a/resources/themes/pterodactyl/admin/servers/index.blade.php b/resources/themes/pterodactyl/admin/servers/index.blade.php index 1c066a4a0..5619ea2f8 100644 --- a/resources/themes/pterodactyl/admin/servers/index.blade.php +++ b/resources/themes/pterodactyl/admin/servers/index.blade.php @@ -42,7 +42,6 @@ ID Server Name Owner - Username Node Connection @@ -52,7 +51,6 @@ {{ $server->uuidShort }} {{ $server->name }} {{ $server->user->username }} - {{ $server->username }} {{ $server->node->name }} {{ $server->allocation->alias }}:{{ $server->allocation->port }} diff --git a/resources/themes/pterodactyl/admin/servers/view/index.blade.php b/resources/themes/pterodactyl/admin/servers/view/index.blade.php index 81d73d06e..d5020bcdf 100644 --- a/resources/themes/pterodactyl/admin/servers/view/index.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/index.blade.php @@ -55,14 +55,6 @@ Docker Container ID - - Docker User ID - - - - Docker Container Name - {{ $server->username }} - Service diff --git a/resources/themes/pterodactyl/server/console.blade.php b/resources/themes/pterodactyl/server/console.blade.php index 20356c272..f50db615f 100644 --- a/resources/themes/pterodactyl/server/console.blade.php +++ b/resources/themes/pterodactyl/server/console.blade.php @@ -16,7 +16,7 @@
-
{{ $server->username }}:~$
+
container:~/$
diff --git a/resources/themes/pterodactyl/server/index.blade.php b/resources/themes/pterodactyl/server/index.blade.php index 6470a7703..9cb4ba4a9 100644 --- a/resources/themes/pterodactyl/server/index.blade.php +++ b/resources/themes/pterodactyl/server/index.blade.php @@ -30,7 +30,7 @@
-
{{ $server->username }}:~$
+
container:~/$
diff --git a/resources/themes/pterodactyl/server/settings/sftp.blade.php b/resources/themes/pterodactyl/server/settings/sftp.blade.php index 3e00bdf57..5da21ef77 100644 --- a/resources/themes/pterodactyl/server/settings/sftp.blade.php +++ b/resources/themes/pterodactyl/server/settings/sftp.blade.php @@ -21,37 +21,7 @@ @section('content')
-
-
-
-

@lang('server.config.sftp.change_pass')

-
- @can('reset-sftp', $server) -
-
-
- -
- -

@lang('auth.password_requirements')

-
-
-
- -
- @else -
-
-

@lang('auth.not_authorized')

-
-
- @endcan -
-
-
+

@lang('server.config.sftp.details')

@@ -66,20 +36,12 @@
- +
- @can('view-sftp-password', $server) -
- -
- sftp_password))value="{{ Crypt::decrypt($server->sftp_password) }}"@endif /> -
-
- @endcan
diff --git a/routes/api-remote.php b/routes/api-remote.php index 28f1edb38..0aa42b1a2 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -12,3 +12,7 @@ Route::group(['prefix' => '/eggs'], function () { Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs'); Route::get('/{uuid}', 'EggRetrievalController@download')->name('api.remote.eggs.download'); }); + +Route::group(['prefix' => '/sftp'], function () { + Route::post('/', 'SftpController@index')->name('api.remote.sftp'); +}); diff --git a/routes/server.php b/routes/server.php index fc658b673..1ceafbe87 100644 --- a/routes/server.php +++ b/routes/server.php @@ -21,10 +21,9 @@ Route::group(['prefix' => 'settings'], function () { Route::get('/allocation', 'Settings\AllocationController@index')->name('server.settings.allocation'); Route::patch('/allocation', 'Settings\AllocationController@update'); - Route::get('/sftp', 'ServerController@getSFTP')->name('server.settings.sftp'); - Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup'); + Route::get('/sftp', 'Settings\SftpController@index')->name('server.settings.sftp'); - Route::post('/sftp', 'ServerController@postSettingsSFTP'); + Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup'); Route::post('/startup', 'ServerController@postSettingsStartup'); }); diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index da2e33af2..89e67d916 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -18,7 +18,6 @@ use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\VariableValidatorService; -use Pterodactyl\Services\Servers\UsernameGenerationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -109,11 +108,6 @@ class ServerCreationServiceTest extends TestCase */ protected $userRepository; - /** - * @var \Pterodactyl\Services\Servers\UsernameGenerationService|\Mockery\Mock - */ - protected $usernameService; - /** * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ @@ -135,7 +129,6 @@ class ServerCreationServiceTest extends TestCase $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->userRepository = m::mock(UserRepositoryInterface::class); - $this->usernameService = m::mock(UsernameGenerationService::class); $this->validatorService = m::mock(VariableValidatorService::class); $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') @@ -150,7 +143,6 @@ class ServerCreationServiceTest extends TestCase $this->repository, $this->serverVariableRepository, $this->userRepository, - $this->usernameService, $this->validatorService ); } @@ -165,8 +157,6 @@ class ServerCreationServiceTest extends TestCase ->shouldReceive('validate')->with($this->data['egg_id'])->once()->andReturnSelf(); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->usernameService->shouldReceive('generate')->with($this->data['name'], 'random_string') - ->once()->andReturn('user_name'); $this->repository->shouldReceive('create')->with(m::subset([ 'uuid' => $this->getKnownUuid(), @@ -211,7 +201,6 @@ class ServerCreationServiceTest extends TestCase { $this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->usernameService->shouldReceive('generate')->once()->andReturn('user_name'); $this->repository->shouldReceive('create')->once()->andReturn((object) [ 'node_id' => 1, 'id' => 1, diff --git a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php deleted file mode 100644 index 61c364338..000000000 --- a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php +++ /dev/null @@ -1,109 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services\Servers; - -use Tests\TestCase; -use phpmock\phpunit\PHPMock; -use Pterodactyl\Services\Servers\UsernameGenerationService; - -class UsernameGenerationServiceTest extends TestCase -{ - use PHPMock; - - /** - * @var UsernameGenerationService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp() - { - parent::setUp(); - - $this->service = new UsernameGenerationService(); - - $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') - ->expects($this->any())->willReturnCallback(function ($count) { - return str_pad('', $count, '0'); - }); - } - - /** - * Test that a valid username is returned and is the correct length. - */ - public function testShouldReturnAValidUsernameWithASelfGeneratedIdentifier() - { - $response = $this->service->generate('testname'); - - $this->assertEquals('testna_00000000', $response); - } - - /** - * Test that a name and identifier provided returns the expected username. - */ - public function testShouldReturnAValidUsernameWithAnIdentifierProvided() - { - $response = $this->service->generate('testname', 'identifier'); - - $this->assertEquals('testna_identifi', $response); - } - - /** - * Test that the identifier is extended to 8 characters if it is shorter. - */ - public function testShouldExtendIdentifierToBe8CharactersIfItIsShorter() - { - $response = $this->service->generate('testname', 'xyz'); - - $this->assertEquals('testna_xyz00000', $response); - } - - /** - * Test that special characters are removed from the username. - */ - public function testShouldStripSpecialCharactersFromName() - { - $response = $this->service->generate('te!st_n$ame', 'identifier'); - - $this->assertEquals('testna_identifi', $response); - } - - /** - * Test that an empty name is replaced with 6 random characters. - */ - public function testEmptyNamesShouldBeReplacedWithRandomCharacters() - { - $response = $this->service->generate(''); - - $this->assertEquals('000000_00000000', $response); - } - - /** - * Test that a name consisting entirely of special characters is handled. - */ - public function testNameOfOnlySpecialCharactersIsHandledProperly() - { - $response = $this->service->generate('$%#*#(@#(#*$&#(#!#@'); - - $this->assertEquals('000000_00000000', $response); - } - - /** - * Test that passing a name shorter than 6 characters returns the entire name. - */ - public function testNameShorterThan6CharactersShouldBeRenderedEntirely() - { - $response = $this->service->generate('test', 'identifier'); - - $this->assertEquals('test_identifi', $response); - } -} diff --git a/tests/Unit/Services/Sftp/AuthenticateUsingPasswordServiceTest.php b/tests/Unit/Services/Sftp/AuthenticateUsingPasswordServiceTest.php new file mode 100644 index 000000000..87ceccd07 --- /dev/null +++ b/tests/Unit/Services/Sftp/AuthenticateUsingPasswordServiceTest.php @@ -0,0 +1,181 @@ +keyProviderService = m::mock(DaemonKeyProviderService::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->userRepository = m::mock(UserRepositoryInterface::class); + } + + /** + * Test that an account can be authenticated. + */ + public function testNonAdminAccountIsAuthenticated() + { + $user = factory(User::class)->make(['root_admin' => 0]); + $server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->keyProviderService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn('server_token'); + + $response = $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + $this->assertNotEmpty($response); + $this->assertArrayHasKey('server', $response); + $this->assertArrayHasKey('token', $response); + $this->assertSame($server->uuid, $response['server']); + $this->assertSame('server_token', $response['token']); + } + + /** + * Test that an administrative user can access servers that they are not + * set as the owner of. + */ + public function testAdminAccountIsAuthenticated() + { + $user = factory(User::class)->make(['root_admin' => 1]); + $server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id + 1]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->keyProviderService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn('server_token'); + + $response = $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + $this->assertNotEmpty($response); + $this->assertArrayHasKey('server', $response); + $this->assertArrayHasKey('token', $response); + $this->assertSame($server->uuid, $response['server']); + $this->assertSame('server_token', $response['token']); + } + + /** + * Test exception gets thrown if no server is passed into the function. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfNoServerIsProvided() + { + $this->getService()->handle('username', 'password', 1); + } + + /** + * Test that an exception is thrown if the user account exists but the wrong + * credentials are passed. + * + * @expectedException \Illuminate\Auth\AuthenticationException + */ + public function testExceptionIsThrownIfUserDetailsAreIncorrect() + { + $user = factory(User::class)->make(); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->getService()->handle($user->username, 'wrongpassword', 1, '1234'); + } + + /** + * Test that an exception is thrown if no user account is found. + * + * @expectedException \Illuminate\Auth\AuthenticationException + */ + public function testExceptionIsThrownIfNoUserAccountIsFound() + { + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', 'something']])->once()->andThrow(new RecordNotFoundException); + + $this->getService()->handle('something', 'password', 1, '1234'); + } + + /** + * Test that an exception is thrown if the user is not the owner of the server + * and is not an administrator. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfUserDoesNotOwnServer() + { + $user = factory(User::class)->make(['root_admin' => 0]); + $server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id + 1]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + } + + /** + * Test that an exception is thrown if the requested server does not belong to + * the node that the request is made from. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfServerDoesNotExistOnCurrentNode() + { + $user = factory(User::class)->make(['root_admin' => 0]); + $server = factory(Server::class)->make(['node_id' => 2, 'owner_id' => $user->id]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService + */ + private function getService(): AuthenticateUsingPasswordService + { + return new AuthenticateUsingPasswordService($this->keyProviderService, $this->repository, $this->userRepository); + } +}