mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 09:34:36 +08:00
fix(registration): time-limit first root account creation
Open root registration for ten minutes after startup, then require a Coolify restart to reopen it. Update installer credential handling and explain the setup window in the registration UI.
This commit is contained in:
1 parent
88e67cbf42
commit
5d66fb719e
14 files changed
+214
-10
No files matched your search
@@ -5,11 +5,13 @@ namespace App\Actions\Fortify;
|
||||
use App\Jobs\SendVerificationEmailJob;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Support\RootSetupWindow;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
@@ -34,6 +36,12 @@ class CreateNewUser implements CreatesNewUsers
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (! User::query()->exists() && ! RootSetupWindow::isOpen()) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => 'The setup window for the root account is closed. Restart Coolify on the server (docker restart coolify) to open it again.',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->ensureRegistrationIsNotRateLimited($input);
|
||||
|
||||
Validator::make($input, [
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\User;
|
||||
use App\Support\ResourceStartActivity;
|
||||
use App\Support\RootSetupWindow;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
@@ -36,6 +37,12 @@ class Init extends Command
|
||||
Artisan::call('optimize:clear');
|
||||
Artisan::call('optimize');
|
||||
|
||||
try {
|
||||
RootSetupWindow::openIfNoUserExists();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not open the root setup window: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$this->pullTemplatesFromCDN();
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Actions\Fortify\UpdateUserPassword;
|
||||
use App\Models\OauthSetting;
|
||||
use App\Models\User;
|
||||
use App\Support\PasskeyRelyingParty;
|
||||
use App\Support\RootSetupWindow;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
@@ -51,6 +52,7 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
|
||||
return view('auth.register', [
|
||||
'isFirstUser' => $isFirstUser,
|
||||
'isRootSetupWindowClosed' => $isFirstUser && ! RootSetupWindow::isOpen(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* The first registration creates the root user. It is allowed only for a short time after
|
||||
* Coolify starts, so a bot cannot claim a fresh install. A restart opens the window again.
|
||||
*/
|
||||
class RootSetupWindow
|
||||
{
|
||||
public const MINUTES = 10;
|
||||
|
||||
private const CACHE_KEY = 'root-setup-window';
|
||||
|
||||
public static function openIfNoUserExists(): void
|
||||
{
|
||||
if (User::query()->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY, true, now()->addMinutes(self::MINUTES));
|
||||
}
|
||||
|
||||
public static function isOpen(): bool
|
||||
{
|
||||
return Cache::has(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,12 @@ class RootUserSeeder extends Seeder
|
||||
return;
|
||||
}
|
||||
|
||||
// The installer writes an empty ROOT_USERNAME line, so an empty value must also use the default.
|
||||
$username = env('ROOT_USERNAME') ?: 'Root User';
|
||||
|
||||
$validator = Validator::make([
|
||||
'email' => env('ROOT_USER_EMAIL'),
|
||||
'username' => env('ROOT_USERNAME', 'Root User'),
|
||||
'username' => $username,
|
||||
'password' => env('ROOT_USER_PASSWORD'),
|
||||
], [
|
||||
'email' => ['required', 'email:rfc,dns', 'max:255'],
|
||||
@@ -48,7 +51,7 @@ class RootUserSeeder extends Seeder
|
||||
try {
|
||||
$user = (new User)->forceFill([
|
||||
'id' => 0,
|
||||
'name' => env('ROOT_USERNAME', 'Root User'),
|
||||
'name' => $username,
|
||||
'email' => env('ROOT_USER_EMAIL'),
|
||||
'password' => Hash::make(env('ROOT_USER_PASSWORD')),
|
||||
]);
|
||||
|
||||
@@ -966,12 +966,16 @@ update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_BACKEND_PORT" "6001"
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
# Add default root user credentials from environment variables (ROOT_USERNAME is optional)
|
||||
if [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
echo " - Setting predefined root user credentials from environment"
|
||||
update_env_var "ROOT_USERNAME" "$(quote_env_value "$ROOT_USERNAME")"
|
||||
if [ -n "$ROOT_USERNAME" ]; then
|
||||
update_env_var "ROOT_USERNAME" "$(quote_env_value "$ROOT_USERNAME")"
|
||||
fi
|
||||
update_env_var "ROOT_USER_EMAIL" "$(quote_env_value "$ROOT_USER_EMAIL")"
|
||||
update_env_var "ROOT_USER_PASSWORD" "$(quote_env_value "$ROOT_USER_PASSWORD")"
|
||||
elif [ -n "$ROOT_USERNAME" ] || [ -n "$ROOT_USER_EMAIL" ] || [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
warn "Predefined root user skipped: set both ROOT_USER_EMAIL and ROOT_USER_PASSWORD."
|
||||
fi
|
||||
|
||||
if [ -n "${REGISTRY_URL+x}" ]; then
|
||||
@@ -1163,6 +1167,9 @@ for IP in $PRIVATE_IPS; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ] && { [ -z "$ROOT_USER_EMAIL" ] || [ -z "$ROOT_USER_PASSWORD" ]; }; then
|
||||
printf '\n %s! Create the root account within 10 minutes. If the time ends, run %sdocker restart coolify%s%s and try again.%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" "$C_YELLOW" "$C_RESET" >&3
|
||||
fi
|
||||
printf '\n %s! Back up %s/data/coolify/source/.env%s%s to a safe place outside this server (e.g. a password manager).%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" "$C_YELLOW" "$C_RESET" >&3
|
||||
printf ' %sLog file: %s%s\n\n' "$C_DIM" "$INSTALLATION_LOG_WITH_DATE" "$C_RESET" >&3
|
||||
|
||||
|
||||
@@ -14,10 +14,20 @@ $email = getOldOrLocal('email', 'test3@example.com');
|
||||
<x-auth.shell title="Coolify"
|
||||
:description="$isFirstUser ? 'Create the root account for this instance.' : 'Create your account to get started.'">
|
||||
<div class="flex flex-col gap-4">
|
||||
@if ($isFirstUser)
|
||||
@if ($isRootSetupWindowClosed)
|
||||
<x-auth.alert type="warning">
|
||||
<p class="font-medium">Setup window closed</p>
|
||||
<p class="mt-0.5 text-black/70 dark:text-white/70">You can create the root account only in the
|
||||
first {{ \App\Support\RootSetupWindow::MINUTES }} minutes after Coolify
|
||||
starts. To open the window again, run this command on the server, then reload this page:</p>
|
||||
<code class="mt-2 block font-mono text-xs">docker restart coolify</code>
|
||||
</x-auth.alert>
|
||||
@elseif ($isFirstUser)
|
||||
<x-auth.alert type="warning">
|
||||
<p class="font-medium">Full instance access</p>
|
||||
<p class="mt-0.5 text-black/70 dark:text-white/70">This first account becomes the root user.</p>
|
||||
<p class="mt-0.5 text-black/70 dark:text-white/70">This first account becomes the root user.
|
||||
Create it within {{ \App\Support\RootSetupWindow::MINUTES }} minutes after
|
||||
Coolify starts.</p>
|
||||
</x-auth.alert>
|
||||
@endif
|
||||
|
||||
@@ -31,6 +41,7 @@ $email = getOldOrLocal('email', 'test3@example.com');
|
||||
</x-auth.alert>
|
||||
@endif
|
||||
|
||||
@unless ($isRootSetupWindowClosed)
|
||||
<form action="/register" method="POST" class="flex flex-col gap-4">
|
||||
@csrf
|
||||
<x-forms.input id="name" required type="text" name="name" value="{{ $name }}" autofocus
|
||||
@@ -51,6 +62,7 @@ $email = getOldOrLocal('email', 'test3@example.com');
|
||||
Create account
|
||||
</x-forms.button>
|
||||
</form>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
@if (! $isFirstUser)
|
||||
|
||||
+10
-3
@@ -966,12 +966,16 @@ update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_BACKEND_PORT" "6001"
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
# Add default root user credentials from environment variables (ROOT_USERNAME is optional)
|
||||
if [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
echo " - Setting predefined root user credentials from environment"
|
||||
update_env_var "ROOT_USERNAME" "$(quote_env_value "$ROOT_USERNAME")"
|
||||
if [ -n "$ROOT_USERNAME" ]; then
|
||||
update_env_var "ROOT_USERNAME" "$(quote_env_value "$ROOT_USERNAME")"
|
||||
fi
|
||||
update_env_var "ROOT_USER_EMAIL" "$(quote_env_value "$ROOT_USER_EMAIL")"
|
||||
update_env_var "ROOT_USER_PASSWORD" "$(quote_env_value "$ROOT_USER_PASSWORD")"
|
||||
elif [ -n "$ROOT_USERNAME" ] || [ -n "$ROOT_USER_EMAIL" ] || [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
warn "Predefined root user skipped: set both ROOT_USER_EMAIL and ROOT_USER_PASSWORD."
|
||||
fi
|
||||
|
||||
if [ -n "${REGISTRY_URL+x}" ]; then
|
||||
@@ -1163,6 +1167,9 @@ for IP in $PRIVATE_IPS; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ] && { [ -z "$ROOT_USER_EMAIL" ] || [ -z "$ROOT_USER_PASSWORD" ]; }; then
|
||||
printf '\n %s! Create the root account within 10 minutes. If the time ends, run %sdocker restart coolify%s%s and try again.%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" "$C_YELLOW" "$C_RESET" >&3
|
||||
fi
|
||||
printf '\n %s! Back up %s/data/coolify/source/.env%s%s to a safe place outside this server (e.g. a password manager).%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" "$C_YELLOW" "$C_RESET" >&3
|
||||
printf ' %sLog file: %s%s\n\n' "$C_DIM" "$INSTALLATION_LOG_WITH_DATE" "$C_RESET" >&3
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\OauthSetting;
|
||||
use App\Support\RootSetupWindow;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Once;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
@@ -10,6 +11,8 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
RootSetupWindow::openIfNoUserExists();
|
||||
|
||||
InstanceSettings::forceCreate([
|
||||
'id' => 0,
|
||||
'is_registration_enabled' => true,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use App\Support\RootSetupWindow;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
|
||||
InstanceSettings::query()->forceCreate([
|
||||
'id' => 0,
|
||||
'is_registration_enabled' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
function registerRootAccount(): TestResponse
|
||||
{
|
||||
return test()->post('/register', [
|
||||
'name' => 'Root User',
|
||||
'email' => 'root@example.com',
|
||||
'password' => 'Password1!@',
|
||||
'password_confirmation' => 'Password1!@',
|
||||
]);
|
||||
}
|
||||
|
||||
it('blocks the root registration when the setup window was never opened', function () {
|
||||
registerRootAccount()->assertSessionHasErrors('email');
|
||||
|
||||
expect(User::count())->toBe(0);
|
||||
});
|
||||
|
||||
it('blocks the root registration after the setup window ends', function () {
|
||||
RootSetupWindow::openIfNoUserExists();
|
||||
$this->travel(RootSetupWindow::MINUTES * 60 + 1)->seconds();
|
||||
|
||||
registerRootAccount()->assertSessionHasErrors('email');
|
||||
|
||||
expect(User::count())->toBe(0);
|
||||
});
|
||||
|
||||
it('creates the root user during the setup window', function () {
|
||||
RootSetupWindow::openIfNoUserExists();
|
||||
$this->travel(RootSetupWindow::MINUTES - 1)->minutes();
|
||||
|
||||
registerRootAccount()->assertSessionHasNoErrors();
|
||||
|
||||
expect(User::find(0)?->email)->toBe('root@example.com');
|
||||
});
|
||||
|
||||
it('shows how to open the setup window again when it is closed', function () {
|
||||
$this->get('/register')
|
||||
->assertOk()
|
||||
->assertSee('docker restart coolify')
|
||||
->assertDontSee('Create account');
|
||||
});
|
||||
|
||||
it('does not open the setup window when a user exists', function () {
|
||||
User::factory()->create(['id' => 0]);
|
||||
|
||||
RootSetupWindow::openIfNoUserExists();
|
||||
|
||||
expect(RootSetupWindow::isOpen())->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RootUserSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::query()->forceCreate(['id' => 0, 'is_registration_enabled' => true]);
|
||||
Team::factory()->create(['id' => 0, 'personal_team' => true]);
|
||||
Http::fake(['api.pwnedpasswords.com/*' => Http::response('')]);
|
||||
|
||||
// The .env template of the installer has an empty ROOT_USERNAME line.
|
||||
putenv('ROOT_USERNAME=');
|
||||
putenv('ROOT_USER_EMAIL=root@coollabs.io');
|
||||
putenv('ROOT_USER_PASSWORD=Rw-Seeder-Test-Aa1!');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
putenv('ROOT_USERNAME');
|
||||
putenv('ROOT_USER_EMAIL');
|
||||
putenv('ROOT_USER_PASSWORD');
|
||||
});
|
||||
|
||||
it('creates the root user when ROOT_USERNAME is empty', function () {
|
||||
ob_start();
|
||||
(new RootUserSeeder)->run();
|
||||
ob_end_clean();
|
||||
|
||||
expect(User::find(0))
|
||||
->not->toBeNull()
|
||||
->name->toBe('Root User')
|
||||
->email->toBe('root@coollabs.io');
|
||||
expect(InstanceSettings::find(0)->is_registration_enabled)->toBeFalse();
|
||||
});
|
||||
@@ -25,6 +25,7 @@ use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Support\RootSetupWindow;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -33,6 +34,8 @@ use Illuminate\Support\Str;
|
||||
*/
|
||||
function seedBrowserInstanceSettings(array $attributes = []): InstanceSettings
|
||||
{
|
||||
RootSetupWindow::openIfNoUserExists();
|
||||
|
||||
return InstanceSettings::forceCreate(array_merge([
|
||||
'id' => 0,
|
||||
'is_sponsorship_popup_enabled' => false,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use App\Support\RootSetupWindow;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Laravel\Fortify\Fortify;
|
||||
@@ -9,6 +10,8 @@ use Laravel\Fortify\Fortify;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
RootSetupWindow::openIfNoUserExists();
|
||||
|
||||
// `id` is not mass-assignable; forceCreate so InstanceSettings::get() (id 0) works.
|
||||
InstanceSettings::forceCreate([
|
||||
'id' => 0,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@@ -18,6 +19,17 @@ it('shows registration page when no users exist', function () {
|
||||
->screenshot(filename: 'registration-root-setup');
|
||||
});
|
||||
|
||||
it('shows how to open the setup window again when it is closed', function () {
|
||||
Cache::flush();
|
||||
|
||||
$page = visit('/register');
|
||||
|
||||
$page->assertSee('Setup window closed')
|
||||
->assertSee('docker restart coolify')
|
||||
->assertDontSee('Create account')
|
||||
->screenshot(filename: 'registration-root-setup-window-closed');
|
||||
});
|
||||
|
||||
it('can register a new root user', function () {
|
||||
$page = visit('/register');
|
||||
|
||||
|
||||
Reference in new issue
Block a user