Fix username validation and auto-generation, closes #927
This commit is contained in:
parent
c3dc376c4c
commit
bf537922a3
7 changed files with 159 additions and 41 deletions
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
36
app/Rules/Username.php
Normal 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.';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
69
tests/Unit/Rules/UsernameTest.php
Normal file
69
tests/Unit/Rules/UsernameTest.php
Normal 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_'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue