fix(server): allow dots in ssh username (#9951)
This commit is contained in:
commit
e31251f5ae
9 changed files with 274 additions and 19 deletions
|
|
@ -13,6 +13,7 @@
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Server as ModelsServer;
|
use App\Models\Server as ModelsServer;
|
||||||
use App\Rules\ValidServerIp;
|
use App\Rules\ValidServerIp;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
|
|
@ -487,10 +488,12 @@ public function create_server(Request $request)
|
||||||
'ip' => ['string', 'required', new ValidServerIp],
|
'ip' => ['string', 'required', new ValidServerIp],
|
||||||
'port' => 'integer|nullable|between:1,65535',
|
'port' => 'integer|nullable|between:1,65535',
|
||||||
'private_key_uuid' => 'string|required',
|
'private_key_uuid' => 'string|required',
|
||||||
'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
'user' => ValidationPatterns::serverUsernameRules(required: false),
|
||||||
'is_build_server' => 'boolean|nullable',
|
'is_build_server' => 'boolean|nullable',
|
||||||
'instant_validate' => 'boolean|nullable',
|
'instant_validate' => 'boolean|nullable',
|
||||||
'proxy_type' => 'string|nullable',
|
'proxy_type' => 'string|nullable',
|
||||||
|
], [
|
||||||
|
...ValidationPatterns::serverUsernameMessages(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
|
|
@ -666,7 +669,7 @@ public function update_server(Request $request)
|
||||||
'ip' => ['string', 'nullable', new ValidServerIp],
|
'ip' => ['string', 'nullable', new ValidServerIp],
|
||||||
'port' => 'integer|nullable|between:1,65535',
|
'port' => 'integer|nullable|between:1,65535',
|
||||||
'private_key_uuid' => 'string|nullable',
|
'private_key_uuid' => 'string|nullable',
|
||||||
'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
'user' => ValidationPatterns::serverUsernameRules(required: false),
|
||||||
'is_build_server' => 'boolean|nullable',
|
'is_build_server' => 'boolean|nullable',
|
||||||
'instant_validate' => 'boolean|nullable',
|
'instant_validate' => 'boolean|nullable',
|
||||||
'proxy_type' => 'string|nullable',
|
'proxy_type' => 'string|nullable',
|
||||||
|
|
@ -676,6 +679,8 @@ public function update_server(Request $request)
|
||||||
'server_disk_usage_notification_threshold' => 'integer|min:1|max:100',
|
'server_disk_usage_notification_threshold' => 'integer|min:1|max:100',
|
||||||
'server_disk_usage_check_frequency' => 'string',
|
'server_disk_usage_check_frequency' => 'string',
|
||||||
'connection_timeout' => 'integer|min:1|max:300',
|
'connection_timeout' => 'integer|min:1|max:300',
|
||||||
|
], [
|
||||||
|
...ValidationPatterns::serverUsernameMessages(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use App\Services\ConfigurationRepository;
|
use App\Services\ConfigurationRepository;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Livewire\Attributes\Url;
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -212,6 +213,23 @@ private function updateServerDetails()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'remoteServerName' => 'required|string',
|
||||||
|
'remoteServerHost' => 'required|string',
|
||||||
|
'remoteServerPort' => 'required|integer|min:1|max:65535',
|
||||||
|
'remoteServerUser' => ValidationPatterns::serverUsernameRules(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...ValidationPatterns::serverUsernameMessages('remoteServerUser', 'SSH User'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function getProxyType()
|
public function getProxyType()
|
||||||
{
|
{
|
||||||
$this->selectProxy(ProxyTypes::TRAEFIK->value);
|
$this->selectProxy(ProxyTypes::TRAEFIK->value);
|
||||||
|
|
@ -274,12 +292,7 @@ public function savePrivateKey()
|
||||||
|
|
||||||
public function saveServer()
|
public function saveServer()
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate();
|
||||||
'remoteServerName' => 'required|string',
|
|
||||||
'remoteServerHost' => 'required|string',
|
|
||||||
'remoteServerPort' => 'required|integer',
|
|
||||||
'remoteServerUser' => 'required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->privateKey = formatPrivateKey($this->privateKey);
|
$this->privateKey = formatPrivateKey($this->privateKey);
|
||||||
$foundServer = Server::whereIp($this->remoteServerHost)->first();
|
$foundServer = Server::whereIp($this->remoteServerHost)->first();
|
||||||
|
|
@ -465,10 +478,10 @@ public function showNewResource()
|
||||||
|
|
||||||
public function saveAndValidateServer()
|
public function saveAndValidateServer()
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate(array_intersect_key($this->rules(), array_flip([
|
||||||
'remoteServerPort' => 'required|integer|min:1|max:65535',
|
'remoteServerPort',
|
||||||
'remoteServerUser' => 'required|string',
|
'remoteServerUser',
|
||||||
]);
|
])));
|
||||||
|
|
||||||
$this->createdServer->update([
|
$this->createdServer->update([
|
||||||
'port' => $this->remoteServerPort,
|
'port' => $this->remoteServerPort,
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ protected function rules(): array
|
||||||
'name' => ValidationPatterns::nameRules(),
|
'name' => ValidationPatterns::nameRules(),
|
||||||
'description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
'ip' => ['required', 'string', new ValidServerIp],
|
'ip' => ['required', 'string', new ValidServerIp],
|
||||||
'user' => ['required', 'string', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
'user' => ValidationPatterns::serverUsernameRules(),
|
||||||
'port' => 'required|integer|between:1,65535',
|
'port' => 'required|integer|between:1,65535',
|
||||||
'is_build_server' => 'required|boolean',
|
'is_build_server' => 'required|boolean',
|
||||||
];
|
];
|
||||||
|
|
@ -75,6 +75,7 @@ protected function messages(): array
|
||||||
'ip.string' => 'The IP Address/Domain must be a string.',
|
'ip.string' => 'The IP Address/Domain must be a string.',
|
||||||
'user.required' => 'The User field is required.',
|
'user.required' => 'The User field is required.',
|
||||||
'user.string' => 'The User field must be a string.',
|
'user.string' => 'The User field must be a string.',
|
||||||
|
...ValidationPatterns::serverUsernameMessages(),
|
||||||
'port.required' => 'The Port field is required.',
|
'port.required' => 'The Port field is required.',
|
||||||
'port.integer' => 'The Port field must be an integer.',
|
'port.integer' => 'The Port field must be an integer.',
|
||||||
'port.between' => 'The Port field must be between 1 and 65535.',
|
'port.between' => 'The Port field must be between 1 and 65535.',
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ protected function rules(): array
|
||||||
'name' => ValidationPatterns::nameRules(),
|
'name' => ValidationPatterns::nameRules(),
|
||||||
'description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
'ip' => ['required', new ValidServerIp],
|
'ip' => ['required', new ValidServerIp],
|
||||||
'user' => ['required', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
'user' => ValidationPatterns::serverUsernameRules(),
|
||||||
'port' => 'required|integer|between:1,65535',
|
'port' => 'required|integer|between:1,65535',
|
||||||
'connectionTimeout' => 'required|integer|min:1|max:300',
|
'connectionTimeout' => 'required|integer|min:1|max:300',
|
||||||
'validationLogs' => 'nullable',
|
'validationLogs' => 'nullable',
|
||||||
|
|
@ -140,6 +140,7 @@ protected function messages(): array
|
||||||
[
|
[
|
||||||
'ip.required' => 'The IP Address field is required.',
|
'ip.required' => 'The IP Address field is required.',
|
||||||
'user.required' => 'The User field is required.',
|
'user.required' => 'The User field is required.',
|
||||||
|
...ValidationPatterns::serverUsernameMessages(),
|
||||||
'port.required' => 'The Port field is required.',
|
'port.required' => 'The Port field is required.',
|
||||||
'connectionTimeout.required' => 'The SSH Connection Timeout field is required.',
|
'connectionTimeout.required' => 'The SSH Connection Timeout field is required.',
|
||||||
'connectionTimeout.integer' => 'The SSH Connection Timeout must be an integer.',
|
'connectionTimeout.integer' => 'The SSH Connection Timeout must be an integer.',
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
use App\Notifications\Server\Reachable;
|
use App\Notifications\Server\Reachable;
|
||||||
use App\Notifications\Server\Unreachable;
|
use App\Notifications\Server\Unreachable;
|
||||||
use App\Services\ConfigurationRepository;
|
use App\Services\ConfigurationRepository;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
|
|
@ -945,10 +946,10 @@ public function user(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: function ($value) {
|
get: function ($value) {
|
||||||
return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
|
return preg_replace(ValidationPatterns::INVALID_SERVER_USERNAME_CHARACTERS_PATTERN, '', $value);
|
||||||
},
|
},
|
||||||
set: function ($value) {
|
set: function ($value) {
|
||||||
return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
|
return preg_replace(ValidationPatterns::INVALID_SERVER_USERNAME_CHARACTERS_PATTERN, '', $value);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,17 @@ class ValidationPatterns
|
||||||
*/
|
*/
|
||||||
public const DOCKER_TARGET_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
|
public const DOCKER_TARGET_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern for SSH usernames.
|
||||||
|
* Allows alphanumeric characters, dots, hyphens, and underscores.
|
||||||
|
*/
|
||||||
|
public const SERVER_USERNAME_PATTERN = '/^[a-zA-Z0-9._-]+$/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern for removing characters not allowed in SSH usernames.
|
||||||
|
*/
|
||||||
|
public const INVALID_SERVER_USERNAME_CHARACTERS_PATTERN = '/[^A-Za-z0-9.\-_]/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token-aware pattern for shell-safe command strings (docker compose commands, docker run options).
|
* Token-aware pattern for shell-safe command strings (docker compose commands, docker run options).
|
||||||
*
|
*
|
||||||
|
|
@ -283,6 +294,28 @@ public static function databaseIdentifierRules(bool $required = true, int $minLe
|
||||||
return $rules;
|
return $rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation rules for SSH username fields.
|
||||||
|
*/
|
||||||
|
public static function serverUsernameRules(bool $required = true): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$required ? 'required' : 'nullable',
|
||||||
|
'string',
|
||||||
|
'regex:'.self::SERVER_USERNAME_PATTERN,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation messages for SSH username fields.
|
||||||
|
*/
|
||||||
|
public static function serverUsernameMessages(string $field = 'user', string $label = 'User'): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
"{$field}.regex" => "The {$label} may only contain letters, numbers, dots, hyphens, and underscores.",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get validation messages for database identifier fields.
|
* Get validation messages for database identifier fields.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1865,15 +1865,15 @@ function isBase64Encoded($strValue)
|
||||||
{
|
{
|
||||||
return base64_encode(base64_decode($strValue, true)) === $strValue;
|
return base64_encode(base64_decode($strValue, true)) === $strValue;
|
||||||
}
|
}
|
||||||
function customApiValidator(Collection|array $item, array $rules)
|
function customApiValidator(Collection|array $item, array $rules, array $messages = [])
|
||||||
{
|
{
|
||||||
if (is_array($item)) {
|
if (is_array($item)) {
|
||||||
$item = collect($item);
|
$item = collect($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Validator::make($item->toArray(), $rules, [
|
return Validator::make($item->toArray(), $rules, array_merge([
|
||||||
'required' => 'This field is required.',
|
'required' => 'This field is required.',
|
||||||
]);
|
], $messages));
|
||||||
}
|
}
|
||||||
function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null)
|
function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
182
tests/Feature/ServerUsernameValidationTest.php
Normal file
182
tests/Feature/ServerUsernameValidationTest.php
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Boarding\Index as BoardingIndex;
|
||||||
|
use App\Livewire\Server\New\ByIp;
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\PrivateKey;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config(['app.maintenance.driver' => 'file']);
|
||||||
|
|
||||||
|
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
||||||
|
|
||||||
|
$this->team = Team::factory()->create();
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||||
|
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
$this->privateKey = PrivateKey::withoutEvents(fn () => PrivateKey::forceCreate([
|
||||||
|
'uuid' => (string) new Cuid2,
|
||||||
|
'name' => 'Test SSH Key',
|
||||||
|
'description' => 'Test SSH Key',
|
||||||
|
'private_key' => 'test-private-key',
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$token = $this->user->createToken('write-token', ['write']);
|
||||||
|
$token->accessToken->forceFill(['team_id' => $this->team->id])->save();
|
||||||
|
$this->token = $token->plainTextToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a server through the API with a dotted SSH username', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->postJson('/api/v1/servers', [
|
||||||
|
'name' => 'Dotted User Server',
|
||||||
|
'ip' => '192.0.2.10',
|
||||||
|
'private_key_uuid' => $this->privateKey->uuid,
|
||||||
|
'user' => 'deploy.user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
$this->assertDatabaseHas('servers', [
|
||||||
|
'ip' => '192.0.2.10',
|
||||||
|
'user' => 'deploy.user',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a server through the API with a dotted SSH username', function () {
|
||||||
|
$server = Server::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'private_key_id' => $this->privateKey->id,
|
||||||
|
'user' => 'deploy',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->patchJson('/api/v1/servers/'.$server->uuid, [
|
||||||
|
'user' => 'deploy.user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
expect($server->fresh()->user)->toBe('deploy.user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsafe SSH usernames when creating a server through the API', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->postJson('/api/v1/servers', [
|
||||||
|
'name' => 'Unsafe User Server',
|
||||||
|
'ip' => '192.0.2.11',
|
||||||
|
'private_key_uuid' => $this->privateKey->uuid,
|
||||||
|
'user' => 'deploy$user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonPath('errors.user.0', 'The User may only contain letters, numbers, dots, hyphens, and underscores.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsafe SSH usernames through the API', function () {
|
||||||
|
$server = Server::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'private_key_id' => $this->privateKey->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->patchJson('/api/v1/servers/'.$server->uuid, [
|
||||||
|
'user' => 'deploy$user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonStructure(['errors' => ['user']]);
|
||||||
|
$response->assertJsonPath('errors.user.0', 'The User may only contain letters, numbers, dots, hyphens, and underscores.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows dotted SSH usernames in the server creation form', function () {
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
Livewire::test(ByIp::class, [
|
||||||
|
'private_keys' => collect([$this->privateKey]),
|
||||||
|
'limit_reached' => false,
|
||||||
|
])
|
||||||
|
->set('name', 'Dotted User Server')
|
||||||
|
->set('ip', '192.0.2.20')
|
||||||
|
->set('user', 'deploy.user')
|
||||||
|
->set('private_key_id', $this->privateKey->id)
|
||||||
|
->call('submit')
|
||||||
|
->assertHasNoErrors(['user']);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('servers', [
|
||||||
|
'ip' => '192.0.2.20',
|
||||||
|
'user' => 'deploy.user',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsafe SSH usernames in the server creation form', function () {
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
Livewire::test(ByIp::class, [
|
||||||
|
'private_keys' => collect([$this->privateKey]),
|
||||||
|
'limit_reached' => false,
|
||||||
|
])
|
||||||
|
->set('name', 'Unsafe User Server')
|
||||||
|
->set('ip', '192.0.2.21')
|
||||||
|
->set('user', 'deploy$user')
|
||||||
|
->set('private_key_id', $this->privateKey->id)
|
||||||
|
->call('submit')
|
||||||
|
->assertHasErrors(['user' => ['regex']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsafe SSH usernames during onboarding server creation', function () {
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
Livewire::test(BoardingIndex::class)
|
||||||
|
->set('createdPrivateKey', $this->privateKey)
|
||||||
|
->set('remoteServerName', 'Unsafe User Server')
|
||||||
|
->set('remoteServerHost', '192.0.2.30')
|
||||||
|
->set('remoteServerPort', 22)
|
||||||
|
->set('remoteServerUser', 'deploy$user')
|
||||||
|
->call('saveServer')
|
||||||
|
->assertHasErrors([
|
||||||
|
'remoteServerUser' => [
|
||||||
|
'regex',
|
||||||
|
'The SSH User may only contain letters, numbers, dots, hyphens, and underscores.',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsafe SSH usernames during onboarding server validation', function () {
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$server = Server::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'private_key_id' => $this->privateKey->id,
|
||||||
|
'user' => 'deploy',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(BoardingIndex::class)
|
||||||
|
->set('createdServer', $server)
|
||||||
|
->set('remoteServerPort', 22)
|
||||||
|
->set('remoteServerUser', 'deploy$user')
|
||||||
|
->call('saveAndValidateServer')
|
||||||
|
->assertHasErrors([
|
||||||
|
'remoteServerUser' => [
|
||||||
|
'regex',
|
||||||
|
'The SSH User may only contain letters, numbers, dots, hyphens, and underscores.',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
19
tests/Unit/ServerUsernamePatternTest.php
Normal file
19
tests/Unit/ServerUsernamePatternTest.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
|
|
||||||
|
it('provides shared validation rules for SSH usernames', function () {
|
||||||
|
expect(ValidationPatterns::SERVER_USERNAME_PATTERN)->toBe('/^[a-zA-Z0-9._-]+$/');
|
||||||
|
expect(ValidationPatterns::serverUsernameRules())->toContain('regex:'.ValidationPatterns::SERVER_USERNAME_PATTERN);
|
||||||
|
|
||||||
|
expect(preg_match(ValidationPatterns::SERVER_USERNAME_PATTERN, 'deploy.user'))->toBe(1);
|
||||||
|
expect(preg_match(ValidationPatterns::SERVER_USERNAME_PATTERN, 'deploy$user'))->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves dots when sanitizing server SSH usernames', function () {
|
||||||
|
$server = new Server;
|
||||||
|
$server->user = 'deploy.user';
|
||||||
|
|
||||||
|
expect($server->user)->toBe('deploy.user');
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue