Fix username validation and auto-generation, closes #927

This commit is contained in:
Dane Everitt 2018-02-11 16:39:50 -06:00
parent c3dc376c4c
commit bf537922a3
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
7 changed files with 159 additions and 41 deletions

View file

@ -9,6 +9,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
* `[rc.2]` — Fixes Admin CP user editing resetting a password on users unintentionally. * `[rc.2]` — Fixes Admin CP user editing resetting a password on users unintentionally.
* `[rc.2]` — Fixes bug with server creation API endpoint that would fail to validate `allocation.default` correctly. * `[rc.2]` — Fixes bug with server creation API endpoint that would fail to validate `allocation.default` correctly.
* `[rc.2]` — Fix data integrity exception occuring due to invalid data being passed to server creation service on the API. * `[rc.2]` — Fix data integrity exception occuring due to invalid data being passed to server creation service on the API.
* `[rc.2]` — Fix data integrity exception that could occur when an email containing non-username characters was passed.
### Added ### Added
* Added ability to search the following API endpoints: list users, list servers, and list locations. * Added ability to search the following API endpoints: list users, list servers, and list locations.

View file

@ -42,7 +42,12 @@ class SftpController extends Controller
*/ */
public function index(SftpAuthenticationFormRequest $request): JsonResponse public function index(SftpAuthenticationFormRequest $request): JsonResponse
{ {
$connection = explode('.', $request->input('username')); $parts = explode('.', strrev($request->input('username')), 2);
$connection = [
'username' => strrev(array_get($parts, 1)),
'server' => strrev(array_get($parts, 0)),
];
$this->incrementLoginAttempts($request); $this->incrementLoginAttempts($request);
if ($this->hasTooManyLoginAttempts($request)) { if ($this->hasTooManyLoginAttempts($request)) {
@ -53,10 +58,10 @@ class SftpController extends Controller
try { try {
$data = $this->authenticationService->handle( $data = $this->authenticationService->handle(
array_get($connection, 0), $connection['username'],
$request->input('password'), $request->input('password'),
object_get($request->attributes->get('node'), 'id', 0), object_get($request->attributes->get('node'), 'id', 0),
array_get($connection, 1) empty($connection['server']) ? null : $connection['server']
); );
$this->clearLoginAttempts($request); $this->clearLoginAttempts($request);

View file

@ -4,6 +4,7 @@ namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable; use Sofa\Eloquence\Validable;
use Pterodactyl\Rules\Username;
use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\In;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -151,7 +152,7 @@ class User extends Model implements
'uuid' => 'string|size:36|unique:users,uuid', 'uuid' => 'string|size:36|unique:users,uuid',
'email' => 'email|unique:users,email', 'email' => 'email|unique:users,email',
'external_id' => 'nullable|string|max:255|unique:users,external_id', 'external_id' => 'nullable|string|max:255|unique:users,external_id',
'username' => 'alpha_dash|between:1,255|unique:users,username', 'username' => 'between:1,255|unique:users,username',
'name_first' => 'string|between:1,255', 'name_first' => 'string|between:1,255',
'name_last' => 'string|between:1,255', 'name_last' => 'string|between:1,255',
'password' => 'nullable|string', 'password' => 'nullable|string',
@ -169,6 +170,7 @@ class User extends Model implements
{ {
$rules = self::eloquenceGatherRules(); $rules = self::eloquenceGatherRules();
$rules['language'][] = new In(array_keys((new self)->getAvailableLanguages())); $rules['language'][] = new In(array_keys((new self)->getAvailableLanguages()));
$rules['username'][] = new Username;
return $rules; return $rules;
} }
@ -188,9 +190,9 @@ class User extends Model implements
* *
* @param string $value * @param string $value
*/ */
public function setUsernameAttribute($value) public function setUsernameAttribute(string $value)
{ {
$this->attributes['username'] = strtolower($value); $this->attributes['username'] = mb_strtolower($value);
} }
/** /**

36
app/Rules/Username.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace Pterodactyl\Rules;
use Illuminate\Contracts\Validation\Rule;
class Username implements Rule
{
public const VALIDATION_REGEX = '/^[a-z0-9]([\w\.-]+)[a-z0-9]$/';
/**
* Validate that a username contains only the allowed characters and starts/ends
* with alpha-numeric characters.
*
* Allowed characters: a-z0-9_-.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value): bool
{
return preg_match(self::VALIDATION_REGEX, mb_strtolower($value));
}
/**
* Return a validation message for use when this rule fails.
*
* @return string
*/
public function message(): string
{
return 'The :attribute must start and end with alpha-numeric characters and
contain only letters, numbers, dashes, underscores, and periods.';
}
}

View file

@ -10,6 +10,7 @@
namespace Pterodactyl\Services\Subusers; namespace Pterodactyl\Services\Subusers;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Rules\Username;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Users\UserCreationService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
@ -117,9 +118,10 @@ class SubuserCreationService
throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists')); throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists'));
} }
} catch (RecordNotFoundException $exception) { } catch (RecordNotFoundException $exception) {
$username = preg_replace('/([^\w\.-]+)/', '', strtok($email, '@'));
$user = $this->userCreationService->handle([ $user = $this->userCreationService->handle([
'email' => $email, 'email' => $email,
'username' => substr(strtok($email, '@'), 0, 8) . '_' . str_random(6), 'username' => $username . str_random(3),
'name_first' => 'Server', 'name_first' => 'Server',
'name_last' => 'Subuser', 'name_last' => 'Subuser',
'root_admin' => false, 'root_admin' => false,

View file

@ -0,0 +1,69 @@
<?php
namespace Tests\Unit\Rules;
use Tests\TestCase;
use Pterodactyl\Rules\Username;
class UsernameTest extends TestCase
{
/**
* Test valid usernames.
*
* @dataProvider validUsernameDataProvider
*/
public function testValidUsernames(string $username)
{
$this->assertTrue((new Username)->passes('test', $username), 'Assert username is valid.');
}
/**
* Test invalid usernames return false.
*
* @dataProvider invalidUsernameDataProvider
*/
public function testInvalidUsernames(string $username)
{
$this->assertFalse((new Username)->passes('test', $username), 'Assert username is not valid.');
}
/**
* Provide valid usernames.
* @return array
*/
public function validUsernameDataProvider(): array
{
return [
['username'],
['user_name'],
['user.name'],
['user-name'],
['123username123'],
['123-user.name'],
['123456'],
];
}
/**
* Provide invalid usernames.
*
* @return array
*/
public function invalidUsernameDataProvider(): array
{
return [
['_username'],
['username_'],
['_username_'],
['-username'],
['.username'],
['username-'],
['username.'],
['user*name'],
['user^name'],
['user#name'],
['user+name'],
['1234_'],
];
}
}

View file

@ -1,17 +1,9 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Subusers; namespace Tests\Unit\Services\Subusers;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use phpmock\phpunit\PHPMock;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser; use Pterodactyl\Models\Subuser;
@ -30,8 +22,6 @@ use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException;
class SubuserCreationServiceTest extends TestCase class SubuserCreationServiceTest extends TestCase
{ {
use PHPMock;
/** /**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/ */
@ -79,8 +69,6 @@ class SubuserCreationServiceTest extends TestCase
{ {
parent::setUp(); parent::setUp();
$this->getFunctionMock('\\Pterodactyl\\Services\\Subusers', 'str_random')->expects($this->any())->willReturn('random_string');
$this->connection = m::mock(ConnectionInterface::class); $this->connection = m::mock(ConnectionInterface::class);
$this->keyCreationService = m::mock(DaemonKeyCreationService::class); $this->keyCreationService = m::mock(DaemonKeyCreationService::class);
$this->permissionService = m::mock(PermissionCreationService::class); $this->permissionService = m::mock(PermissionCreationService::class);
@ -88,16 +76,6 @@ class SubuserCreationServiceTest extends TestCase
$this->serverRepository = m::mock(ServerRepositoryInterface::class); $this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->userCreationService = m::mock(UserCreationService::class); $this->userCreationService = m::mock(UserCreationService::class);
$this->userRepository = m::mock(UserRepositoryInterface::class); $this->userRepository = m::mock(UserRepositoryInterface::class);
$this->service = new SubuserCreationService(
$this->connection,
$this->keyCreationService,
$this->permissionService,
$this->serverRepository,
$this->subuserRepository,
$this->userCreationService,
$this->userRepository
);
} }
/** /**
@ -107,18 +85,25 @@ class SubuserCreationServiceTest extends TestCase
{ {
$permissions = ['test-1' => 'test:1', 'test-2' => null]; $permissions = ['test-1' => 'test:1', 'test-2' => null];
$server = factory(Server::class)->make(); $server = factory(Server::class)->make();
$user = factory(User::class)->make(); $user = factory(User::class)->make([
'email' => 'known.1+test@example.com',
]);
$subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andThrow(new RecordNotFoundException); $this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andThrow(new RecordNotFoundException);
$this->userCreationService->shouldReceive('handle')->with([ $this->userCreationService->shouldReceive('handle')->with(m::on(function ($data) use ($user) {
$subset = m::subset([
'email' => $user->email, 'email' => $user->email,
'username' => substr(strtok($user->email, '@'), 0, 8) . '_' . 'random_string',
'name_first' => 'Server', 'name_first' => 'Server',
'name_last' => 'Subuser', 'name_last' => 'Subuser',
'root_admin' => false, 'root_admin' => false,
])->once()->andReturn($user); ])->match($data);
$username = substr(array_get($data, 'username', ''), 0, -3) === 'known.1test';
return $subset && $username;
}))->once()->andReturn($user);
$this->subuserRepository->shouldReceive('create')->with(['user_id' => $user->id, 'server_id' => $server->id]) $this->subuserRepository->shouldReceive('create')->with(['user_id' => $user->id, 'server_id' => $server->id])
->once()->andReturn($subuser); ->once()->andReturn($subuser);
@ -126,7 +111,7 @@ class SubuserCreationServiceTest extends TestCase
$this->permissionService->shouldReceive('handle')->with($subuser->id, array_keys($permissions))->once()->andReturnNull(); $this->permissionService->shouldReceive('handle')->with($subuser->id, array_keys($permissions))->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->handle($server, $user->email, array_keys($permissions)); $response = $this->getService()->handle($server, $user->email, array_keys($permissions));
$this->assertInstanceOf(Subuser::class, $response); $this->assertInstanceOf(Subuser::class, $response);
$this->assertSame($subuser, $response); $this->assertSame($subuser, $response);
} }
@ -155,7 +140,7 @@ class SubuserCreationServiceTest extends TestCase
$this->permissionService->shouldReceive('handle')->with($subuser->id, $permissions)->once()->andReturnNull(); $this->permissionService->shouldReceive('handle')->with($subuser->id, $permissions)->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->handle($server->id, $user->email, $permissions); $response = $this->getService()->handle($server->id, $user->email, $permissions);
$this->assertInstanceOf(Subuser::class, $response); $this->assertInstanceOf(Subuser::class, $response);
$this->assertSame($subuser, $response); $this->assertSame($subuser, $response);
} }
@ -172,7 +157,7 @@ class SubuserCreationServiceTest extends TestCase
$this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); $this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user);
try { try {
$this->service->handle($server, $user->email, []); $this->getService()->handle($server, $user->email, []);
} catch (DisplayException $exception) { } catch (DisplayException $exception) {
$this->assertInstanceOf(UserIsServerOwnerException::class, $exception); $this->assertInstanceOf(UserIsServerOwnerException::class, $exception);
$this->assertEquals(trans('exceptions.subusers.user_is_owner'), $exception->getMessage()); $this->assertEquals(trans('exceptions.subusers.user_is_owner'), $exception->getMessage());
@ -195,10 +180,28 @@ class SubuserCreationServiceTest extends TestCase
])->once()->andReturn(1); ])->once()->andReturn(1);
try { try {
$this->service->handle($server, $user->email, []); $this->getService()->handle($server, $user->email, []);
} catch (DisplayException $exception) { } catch (DisplayException $exception) {
$this->assertInstanceOf(ServerSubuserExistsException::class, $exception); $this->assertInstanceOf(ServerSubuserExistsException::class, $exception);
$this->assertEquals(trans('exceptions.subusers.subuser_exists'), $exception->getMessage()); $this->assertEquals(trans('exceptions.subusers.subuser_exists'), $exception->getMessage());
} }
} }
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Subusers\SubuserCreationService
*/
private function getService(): SubuserCreationService
{
return new SubuserCreationService(
$this->connection,
$this->keyCreationService,
$this->permissionService,
$this->serverRepository,
$this->subuserRepository,
$this->userCreationService,
$this->userRepository
);
}
} }