Skip to content

Commit 670d381

Browse files
authored
Sites must have isolated users (#980)
* Sites must have isolated users * improve ux
1 parent f61b5c0 commit 670d381

File tree

5 files changed

+48
-57
lines changed

5 files changed

+48
-57
lines changed

app/Actions/Site/CreateSite.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function create(Server $server, array $input): Site
3030

3131
DB::beginTransaction();
3232
try {
33-
$user = $input['user'] ?? $server->getSshUser();
33+
$user = $input['user'];
3434
$site = new Site([
3535
'server_id' => $server->id,
3636
'type' => $input['type'],
@@ -110,7 +110,7 @@ private function validate(Server $server, array $input): void
110110
new DomainRule,
111111
],
112112
'user' => [
113-
'nullable',
113+
'required',
114114
'regex:/^[a-z_][a-z0-9_-]*[a-z0-9]$/',
115115
'min:3',
116116
'max:32',

resources/js/components/ui/password-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(({
2929
/>
3030
<button
3131
type="button"
32-
className="absolute right-0 top-0 flex h-9 w-9 items-center justify-center text-muted-foreground hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
32+
className="text-muted-foreground hover:text-foreground absolute top-0 right-0 flex h-9 w-9 items-center justify-center disabled:pointer-events-none disabled:opacity-50"
3333
onClick={() => setShowPassword((prev) => !prev)}
3434
disabled={props.disabled}
3535
aria-label={showPassword ? 'Hide password' : 'Show password'}

resources/js/pages/sites/components/create-site.tsx

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { Form, FormField, FormFields } from '@/components/ui/form';
44
import { Button } from '@/components/ui/button';
55
import { Label } from '@/components/ui/label';
66
import { Input } from '@/components/ui/input';
7-
import { LoaderCircle } from 'lucide-react';
7+
import { LoaderCircle, HelpCircle } from 'lucide-react';
8+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
9+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
810
import { useForm, usePage } from '@inertiajs/react';
911
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
1012
import InputError from '@/components/ui/input-error';
@@ -281,19 +283,46 @@ export default function CreateSite({
281283
))}
282284
</FormField>
283285

284-
{page.props.configs.site.types[form.data.type].form?.map((config) => getFormField(config))}
285-
286286
<FormField>
287-
<Label htmlFor="user">Isolated User (Optional)</Label>
287+
<Label htmlFor="user" className="flex items-center gap-1">
288+
Isolated User
289+
<Dialog>
290+
<TooltipProvider>
291+
<Tooltip>
292+
<TooltipTrigger asChild>
293+
<DialogTrigger asChild>
294+
<button type="button" className="text-muted-foreground hover:text-foreground">
295+
<HelpCircle className="h-4 w-4" />
296+
</button>
297+
</DialogTrigger>
298+
</TooltipTrigger>
299+
<TooltipContent>Why?</TooltipContent>
300+
</Tooltip>
301+
</TooltipProvider>
302+
<DialogContent>
303+
<DialogHeader>
304+
<DialogTitle>Why Isolated Users?</DialogTitle>
305+
<DialogDescription>
306+
Isolated users are mandatory to ensure security for your sites. If a site has security vulnerabilities and gets
307+
compromised, the attacker cannot take full control of the server because the site runs under its own isolated user
308+
with limited permissions.
309+
</DialogDescription>
310+
</DialogHeader>
311+
</DialogContent>
312+
</Dialog>
313+
</Label>
288314
<Input
289315
id="user"
290316
type="text"
291317
value={form.data.user}
292318
onChange={(e) => form.setData('user', e.target.value)}
293-
placeholder="Leave empty for using server's default user"
319+
placeholder="e.g. mysite"
294320
/>
321+
<p className="text-muted-foreground text-xs">The isolated user for the site. Must be unique on the server.</p>
295322
<InputError message={form.errors.user} />
296323
</FormField>
324+
325+
{page.props.configs.site.types[form.data.type].form?.map((config) => getFormField(config))}
297326
</>
298327
)}
299328
</FormFields>

tests/Feature/API/SitesTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ public function test_create_site(array $inputs): void
7373
->assertJsonFragment([
7474
'domain' => $inputs['domain'],
7575
'aliases' => $inputs['aliases'] ?? [],
76-
'user' => $inputs['user'] ?? $this->server->getSshUser(),
77-
'path' => '/home/'.($inputs['user'] ?? $this->server->getSshUser()).'/'.$inputs['domain'],
76+
'user' => $inputs['user'],
77+
'path' => '/home/'.$inputs['user'].'/'.$inputs['domain'],
7878
]);
7979
}
8080

tests/Feature/SitesTest.php

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,12 @@ public function test_create_site(array $inputs): void
6262
$this->post(route('sites.store', ['server' => $this->server]), $inputs)
6363
->assertSessionDoesntHaveErrors();
6464

65-
$expectedUser = empty($inputs['user']) ? $this->server->getSshUser() : $inputs['user'];
6665
$this->assertDatabaseHas('sites', [
6766
'domain' => $inputs['domain'],
6867
'aliases' => $this->castAsJson($inputs['aliases'] ?? []),
6968
'status' => SiteStatus::READY->value,
70-
'user' => $expectedUser,
71-
'path' => '/home/'.$expectedUser.'/'.$inputs['domain'],
69+
'user' => $inputs['user'],
70+
'path' => '/home/'.$inputs['user'].'/'.$inputs['domain'],
7271
]);
7372
}
7473

@@ -97,6 +96,7 @@ public function test_create_site_failed_due_to_source_control(int $status): void
9796
'repository' => 'test/test',
9897
'branch' => 'main',
9998
'composer' => true,
99+
'user' => 'example',
100100
];
101101

102102
SSH::fake();
@@ -421,6 +421,7 @@ public function test_create_site_with_valid_web_directory(): void
421421
'domain' => 'example.com',
422422
'php_version' => '8.2',
423423
'web_directory' => 'public/dist',
424+
'user' => 'example',
424425
])
425426
->assertSessionDoesntHaveErrors();
426427

@@ -441,6 +442,7 @@ public function test_create_site_with_special_characters_web_directory(): void
441442
'domain' => 'example.com',
442443
'php_version' => '8.2',
443444
'web_directory' => 'public-dist_v1.0',
445+
'user' => 'example',
444446
])
445447
->assertSessionDoesntHaveErrors();
446448

@@ -461,6 +463,7 @@ public function test_create_site_normalizes_web_directory_slashes(): void
461463
'domain' => 'example.com',
462464
'php_version' => '8.2',
463465
'web_directory' => '/public/',
466+
'user' => 'example',
464467
])
465468
->assertSessionDoesntHaveErrors();
466469

@@ -481,6 +484,7 @@ public function test_create_site_normalizes_root_web_directory(): void
481484
'domain' => 'example.com',
482485
'php_version' => '8.2',
483486
'web_directory' => '/',
487+
'user' => 'example',
484488
])
485489
->assertSessionDoesntHaveErrors();
486490

@@ -501,6 +505,7 @@ public function test_create_site_rejects_invalid_web_directory_characters(): voi
501505
'domain' => 'example.com',
502506
'php_version' => '8.2',
503507
'web_directory' => 'public@invalid!',
508+
'user' => 'example',
504509
])
505510
->assertSessionHasErrors(['web_directory']);
506511

@@ -520,6 +525,7 @@ public function test_create_site_rejects_directory_traversal(): void
520525
'domain' => 'example.com',
521526
'php_version' => '8.2',
522527
'web_directory' => '../etc/passwd',
528+
'user' => 'example',
523529
])
524530
->assertSessionHasErrors(['web_directory']);
525531

@@ -593,18 +599,6 @@ public static function failure_create_data(): array
593599
public static function create_data(): array
594600
{
595601
return [
596-
[
597-
[
598-
'type' => Laravel::id(),
599-
'domain' => 'example.com',
600-
'aliases' => ['www.example.com', 'www2.example.com'],
601-
'php_version' => '8.2',
602-
'web_directory' => 'public',
603-
'repository' => 'test/test',
604-
'branch' => 'main',
605-
'composer' => true,
606-
],
607-
],
608602
[
609603
[
610604
'type' => Laravel::id(),
@@ -618,20 +612,6 @@ public static function create_data(): array
618612
'user' => 'example',
619613
],
620614
],
621-
[
622-
[
623-
'type' => Wordpress::id(),
624-
'domain' => 'example.com',
625-
'aliases' => ['www.example.com'],
626-
'php_version' => '8.2',
627-
'title' => 'Example',
628-
'username' => 'example',
629-
'email' => 'email@example.com',
630-
'password' => 'password',
631-
'database' => '1',
632-
'database_user' => '1',
633-
],
634-
],
635615
[
636616
[
637617
'type' => Wordpress::id(),
@@ -647,15 +627,6 @@ public static function create_data(): array
647627
'user' => 'example',
648628
],
649629
],
650-
[
651-
[
652-
'type' => PHPBlank::id(),
653-
'domain' => 'example.com',
654-
'aliases' => ['www.example.com'],
655-
'php_version' => '8.2',
656-
'web_directory' => 'public',
657-
],
658-
],
659630
[
660631
[
661632
'type' => PHPBlank::id(),
@@ -666,15 +637,6 @@ public static function create_data(): array
666637
'user' => 'example',
667638
],
668639
],
669-
[
670-
[
671-
'type' => PHPMyAdmin::id(),
672-
'domain' => 'example.com',
673-
'aliases' => ['www.example.com'],
674-
'php_version' => '8.2',
675-
'version' => '5.1.2',
676-
],
677-
],
678640
[
679641
[
680642
'type' => PHPMyAdmin::id(),

0 commit comments

Comments
 (0)